foxclaw 0.1.3 → 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 (49) hide show
  1. package/index.js +1 -1
  2. package/package.json +1 -1
  3. package/python/.github/workflows/release.yml +5 -0
  4. package/python/CHANGELOG.md +43 -0
  5. package/python/CHANGELOG_CN.md +43 -0
  6. package/python/README.md +6 -0
  7. package/python/README_CN.md +6 -0
  8. package/python/docs/CONTENT_GUARD.md +189 -0
  9. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  10. package/python/docs/RELEASE.md +74 -34
  11. package/python/foxclaw/__init__.py +41 -1
  12. package/python/foxclaw/agents/base.py +17 -0
  13. package/python/foxclaw/agents/mixins/memory.py +16 -2
  14. package/python/foxclaw/agents/mixins/prompt.py +59 -6
  15. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  16. package/python/foxclaw/api/bootstrap.py +20 -2
  17. package/python/foxclaw/api/routes/memory.py +22 -0
  18. package/python/foxclaw/behavior/__init__.py +20 -0
  19. package/python/foxclaw/behavior/behavior.yaml +62 -0
  20. package/python/foxclaw/behavior/spec.py +287 -0
  21. package/python/foxclaw/core/content_guard.py +341 -0
  22. package/python/foxclaw/core/memory_adapter.py +24 -4
  23. package/python/foxclaw/core/model_router.py +38 -2
  24. package/python/foxclaw/core/sensitive_write.py +283 -0
  25. package/python/frontend/planet/planet.js +18 -0
  26. package/python/frontend/v3/assets/{index-D5nIsjms.css → index-JKfHlb-a.css} +1 -1
  27. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  28. package/python/frontend/v3/index.html +2 -2
  29. package/python/frontend_src/package.json +1 -1
  30. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -2
  31. package/python/frontend_src/src/stores/panels.js +16 -3
  32. package/python/pyproject.toml +12 -1
  33. package/python/tests/run_all.py +1 -0
  34. package/python/tests/test_agent_rules.py +81 -0
  35. package/python/tests/test_behavior_spec.py +198 -0
  36. package/python/tests/test_conflict_dismiss.py +143 -0
  37. package/python/tests/test_content_guard.py +177 -0
  38. package/python/tests/test_sensitive_write.py +181 -0
  39. package/python/tools/build_content_lexicon.py +218 -0
  40. package/python/tools/bundle_web.py +98 -0
  41. package/python/tools/recheck_conflicts.py +156 -0
  42. package/python/tools/release_check.py +17 -8
  43. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  44. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  45. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  46. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  47. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  48. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  49. package/python/frontend/v3/assets/index-CQSBdxk4.js +0 -34
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export const name = "foxclaw";
2
- export const version = "0.1.3";
2
+ export const version = "0.1.4";
3
3
 
4
4
  export default { name, version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foxclaw",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A personal agent with memory - a fox living in your device",
5
5
  "keywords": [
6
6
  "agent",
@@ -83,6 +83,11 @@ jobs:
83
83
  python tools/release_check.py
84
84
  fi
85
85
 
86
+ - name: Bundle web UI into package
87
+ # frontend/ 在仓库根、不属于任何包,不复制就进不了 wheel → pip 装完首页 404。
88
+ # 漏跑不会构建失败,但下一步的 artifact gate 会拦下来(missing_web_ui)。
89
+ run: python tools/bundle_web.py
90
+
86
91
  - name: Build wheel + sdist
87
92
  run: python -m build --outdir dist
88
93
 
@@ -4,6 +4,49 @@ Changes from the first public release 0.1.0 to the current version, newest first
4
4
 
5
5
  Chinese version: [CHANGELOG_CN.md](CHANGELOG_CN.md).
6
6
 
7
+ ## 0.1.4 — 2026-11-12
8
+
9
+ A conflict card can now be dismissed instead of decided, the validator stops reading every Chinese 不 as a negation, each agent gets its own rules file, and the hard boundaries now live in one checkable spec instead of being scattered across prompts.
10
+
11
+ **Behavior**
12
+
13
+ - `foxclaw/behavior/behavior.yaml` is a checkpoint list, not a prose spec: every rule that claims code enforcement names a callable that already exists (`foxclaw.kernel.policy_gate:PolicyGate.check_action`, `foxclaw.core.confirm_nonce:ConfirmationBroker.consume`, `foxclaw.core.verify:verify`), and the spec is resolved at load time. A missing target fails startup instead of registering an always-allow stub, so the list cannot drift into decoration.
14
+ - The block is injected first in the system prompt — ahead of the kernel prompt, the role prompt and the per-agent rules. Persona stays where it was (preferences merged into the last user message), so a user-editable preference cannot structurally override a hard boundary.
15
+ - `behavior.local.yaml` in `FOXCLAW_DATA_DIR` can add rules and tighten `enforce`, but weakening or dropping a rule in `prohibitions` / `confirmations` raises. A malformed or stale-version local file is ignored with a warning rather than bricking the app; problems in the built-in file stay fatal.
16
+ - Rules whose code side is not built yet (`no_privacy_upload`, `no_impersonation`) are marked `enforce: prompt` with a `note` saying what is missing, rather than pretending to be enforced.
17
+ - Set `FOXCLAW_BEHAVIOR=0` to turn the whole block off.
18
+
19
+ **Memory**
20
+
21
+ - High-sensitivity writes to long-term memory now pass through a deterministic gate: credential / ID-number / health / card-number patterns plus a Luhn check, no model call. `FOXCLAW_SENSITIVE_WRITE_MODE` has three levels — `audit` (default, records only), `warn`, and `enforce`. In `enforce` the content is not stored; it becomes a pending item the user must approve, and `approve_sensitive_write` is the only caller allowed to pass `consent=True`.
22
+ - The classifier deliberately over-blocks: a false positive costs one confirmation, a false negative means the rule does not exist. Ordinary amounts and order numbers are excluded on purpose — otherwise the finance agent would be unusable.
23
+ - `GET /api/memory/pending`, `POST /api/memory/pending/{id}/approve` and `/reject` expose the queue. The memory panel lists what is waiting, with 记住 / 丢弃 buttons, and when a write is held back the agent says so in the thought chain instead of silently dropping it.
24
+ - `confirm_high_sensitivity` is now `enforce: both` in the behavior spec; the checkpoint resolves to the real gate.
25
+
26
+ **Content safety**
27
+
28
+ - New gate at `foxclaw/core/content_guard.py`: deterministic, no model call. Both the user request and the model output are checked, and a hit is cut off with a fixed line ("Let's talk about something else." / 「我们换个话题聊聊吧。」) — no explanation, no elaboration, no counter-question, no alternative offered. A refusal must not become a channel for leaking information or continuing the thread.
29
+ - Built-in rules cover **solicitation only** (making drugs or explosives, fraud and money laundering, breaking into someone's systems, forging documents, suicide methods, sexual content involving minors). Narrow wording, low false-positive rate, works out of the box. Mentioning is not asking: a book about the history of explosives, a paper on money-laundering sentencing, an order number, `born in '91` all pass.
30
+ - The platform-review categories (political, sexual, violent) run to tens of thousands of terms; hard-coding them is neither practical nor maintainable as the rules change. Deployers mount an external lexicon through `FOXCLAW_CONTENT_LEXICON` (comma-separated files, one term per line, optional `term<TAB>category`); `tools/build_content_lexicon.py` does the conversion (encoding detection, category filtering, category inferred from the file name, read-back verification), and which categories to mount is their call. **Nothing is checked against it unless it is mounted** — by default only the built-in rules run.
31
+ - `FOXCLAW_CONTENT_GUARD_MODE` has three levels: `enforce` (default), `audit` (records only), `off`. Enforcement is the default because this layer exists to backstop a shipped product; a default that does not fire is not a backstop. The built-in rules are narrow enough not to interrupt ordinary conversation.
32
+ - Normalization defeats full-width characters, zero-width characters and inserted spaces. Pinyin, homophones and character-splitting are explicitly out of scope for a deterministic layer — this is a speed bump, not a wall.
33
+ - Knowledge-base ingestion and attachment bodies are **not** filtered: that is the user's own material. The gate sits on the request and the output only, so a history book or a legal statute is not mistaken for a violation.
34
+ - New `no_harmful_content` rule in the behavior spec (`enforce: both`), and a new §8 "content boundaries" in `KERNEL_PROMPT` spelling out the refusal pattern and the difference between 「material」 and 「instructions」.
35
+ - `docs/CONTENT_GUARD.md` / `docs/CONTENT_GUARD_CN.md` document mounting, lexicon format, conversion and false-positive tuning. No political, sexual or violent list is bundled; mounting one, and which mode to run, is the deployer's decision.
36
+
37
+ **Agents**
38
+
39
+ - An agent now loads `foxclaw/agents/rules/<AGENT_ID>/*.md` into its system prompt when it starts. The files are re-read on every prompt assembly, so editing rules needs no code change; the block is appended after the role prompt and yields to the safety rules above it. Set `FOXCLAW_AGENT_RULES=0` to turn it off.
40
+ - Ships with `foxclaw/agents/rules/finance/RULES.md` — Personal Finance Agent Rules v1.0 (data truthfulness, bookkeeping accuracy, privacy, calculation conventions, advice limits, anomaly handling, memory boundaries, supervisor collaboration). The file is package data, so it travels inside the wheel instead of being silently dropped.
41
+
42
+ **Knowledge**
43
+
44
+ - `POST /conflicts/{id}/dismiss` and `POST /conflicts/bulk-dismiss` (SDK: `dismiss_conflict` / `dismiss_conflicts`) close a card without picking a winner: both claims stay `active` and nothing is marked `superseded`. A claim that was `disputed` only because of that card goes back to `active` once no open conflict mentions it.
45
+ - Conflict detection no longer flips polarity on topical similarity alone. The negated side now also has to overlap lexically with the other statement (character-bigram Jaccard ≥ 0.40) and stay close in embedding space (cosine ≥ 0.35); both thresholds are constructor parameters (`neg_flip_min_cos`, `neg_flip_min_overlap`).
46
+ - Descriptive `不` / `无` compounds (`不变`, `不同`, `不管`, `不再`, `无需`, `不可观测`, `不可控` …) no longer count as stance negation.
47
+ - The conflict card gained a secondary "false positive" button beside "accept selected claim".
48
+ - `tools/recheck_conflicts.py` re-scores existing open cards with the current rule and batch-closes those that no longer hold. It dry-runs by default; `--apply` submits through the running service, so the in-process conflict set and the vector index stay in step.
49
+
7
50
  ## 0.1.3 — 2026-11-11
8
51
 
9
52
  Bilingual interface, and Settings moved where it belongs.
@@ -4,6 +4,49 @@
4
4
 
5
5
  英文版:[CHANGELOG.md](CHANGELOG.md)。
6
6
 
7
+ ## 0.1.4 — 2026-11-12
8
+
9
+ 冲突卡可以「判为误判」,不必二选一;判定逻辑也不再见到「不」就当否定;每个 Agent 有了自己的规则文件;硬边界集中到一份可校验的清单,不再散落在各处提示词里。
10
+
11
+ **行为规范**
12
+
13
+ - `foxclaw/behavior/behavior.yaml` 是**检查点清单**,不是规范全文:每条声称代码强制的规则都必须指向仓库里已经存在的可调用对象(`foxclaw.kernel.policy_gate:PolicyGate.check_action`、`foxclaw.core.confirm_nonce:ConfirmationBroker.consume`、`foxclaw.core.verify:verify`),加载时解析。解析不到直接启动失败,绝不注册「永远放行」的桩,所以清单不会退化成装饰。
14
+ - 该段注入 system prompt 最前段,排在内核准则、角色职责、规则文件之前。人格仍在原地(偏好合并进最后一条 user 消息),因此用户可改的偏好结构上覆盖不了硬边界。
15
+ - `FOXCLAW_DATA_DIR` 下的 `behavior.local.yaml` 可以新增规则、可以把 `enforce` 收紧;但放宽或省略删除 `prohibitions` / `confirmations` 里的规则会直接报错。本地文件语法写错或版本过期只告警并忽略,不会把应用锁死;内置文件的问题仍然致命。
16
+ - 代码侧尚未实现的规则(`no_privacy_upload`、`no_impersonation`)标成 `enforce: prompt` 并附 `note` 说明缺什么,不假装已经强制。
17
+ - 设 `FOXCLAW_BEHAVIOR=0` 可整体关闭。
18
+
19
+ **记忆**
20
+
21
+ - 高敏感内容写入长期记忆前先过一道确定性闸门:凭据 / 证件号 / 健康 / 卡号模式加 Luhn 校验,不调模型。`FOXCLAW_SENSITIVE_WRITE_MODE` 三档:`audit`(默认,只记录)、`warn`、`enforce`。`enforce` 下内容**不落库**,转成待确认项由用户点头;`approve_sensitive_write` 是全仓唯一允许传 `consent=True` 的地方。
22
+ - 判定刻意宁可多拦:误判的代价是用户多点一次确认,漏判则等于这条规矩不存在。普通金额和订单号被明确排除,否则财务 Agent 直接不可用。
23
+ - 新增 `GET /api/memory/pending`、`POST /api/memory/pending/{id}/approve` 与 `/reject`;记忆中心面板列出排队中的写入,可就地「记住」或「丢弃」。写入被拦时 Agent 会在思考链里说明,不静默丢弃。
24
+ - 行为规范里 `confirm_high_sensitivity` 升为 `enforce: both`,检查点指向真实闸门。
25
+
26
+ **内容安全**
27
+
28
+ - 新增内容闸门 `foxclaw/core/content_guard.py`:确定性判定,不调模型。用户请求与模型输出两端各过一道,命中即用固定话术切断(「我们换个话题聊聊吧。」/ "Let's talk about something else."),不解释、不延伸、不反问、不给替代方案 —— 拒绝本身不能变成信息泄漏或继续纠缠的入口。
29
+ - 内置规则只覆盖**请求型教唆**(制毒制爆、诈骗洗钱、入侵他人系统、伪造证件、自杀方法、未成年性内容),措辞窄、误伤低,开箱即用。「提及」不算「索要」:讲炸药发展史的书、洗钱量刑的论文、订单号、`91年出生` 都不拦。
30
+ - 平台审核要求的政治 / 色情 / 暴力大类词表在数万条量级,硬编码进代码既不现实也无法随口径更新。部署方用 `FOXCLAW_CONTENT_LEXICON` 挂载外部词表(逗号分隔多个文件,一行一词,可选 `词<TAB>类别`);`tools/build_content_lexicon.py` 负责格式转换(自动判别编码、按类别筛选、按文件名推类别、转换后回读验证),挂哪几类由部署方自己定。**这一层不挂就不生效**:默认只有内置规则在跑。
31
+ - `FOXCLAW_CONTENT_GUARD_MODE` 三档:`enforce`(默认)、`audit`(只记录)、`off`。默认强制,因为这是给对外产品兜底的机制,默认不生效等于没做;内置规则范围窄,正常对话不会被误伤。
32
+ - 归一化对抗全角、零宽字符、空格插字。拼音、谐音、拆字改写不在确定性层处理 —— 这里明确只做限速带,不做墙。
33
+ - 知识库导入与附件正文**不过滤**:那是用户自己的资料。过滤器只挂在「用户请求」和「模型输出」两端,避免把历史书和法律条文当成违规请求。
34
+ - 行为规范新增 `no_harmful_content`(`enforce: both`),`KERNEL_PROMPT` 增加【八、内容边界】写明拒绝范式,并区分「资料」与「指令」两种角色。
35
+ - 新增 `docs/CONTENT_GUARD_CN.md` / `docs/CONTENT_GUARD.md`:挂载方式、词表格式、格式转换与误伤调优。仓库不附带任何政治 / 色情 / 暴力词表,是否挂载、跑哪一档由部署方决定。
36
+
37
+ **Agent**
38
+
39
+ - Agent 启动时会把 `foxclaw/agents/rules/<AGENT_ID>/*.md` 读进系统提示词。每次组装提示词都重新读盘,改规则不必改代码;该段附在角色职责之后,与上游安全规则冲突时以安全规则为准。设 `FOXCLAW_AGENT_RULES=0` 可整体关闭。
40
+ - 随包附带 `foxclaw/agents/rules/finance/RULES.md` —— Personal Finance Agent Rules v1.0(数据真实性、账目准确性、隐私、计算口径、建议边界、异常处理、记忆边界、与主管协作)。该文件属 package data,随 wheel 一起分发,不会像仓库根的 `skills/` 那样漏包。
41
+
42
+ **知识**
43
+
44
+ - 新增 `POST /conflicts/{id}/dismiss` 与 `POST /conflicts/bulk-dismiss`(SDK:`dismiss_conflict` / `dismiss_conflicts`),关闭卡片但不判胜负:双方 claim 保持 `active`,没有一方被标成 `superseded`。仅因该卡被置为 `disputed` 的观点,在不牵涉任何 open 冲突后恢复为 `active`。
45
+ - 冲突判定不再只凭主题相似就翻转立场:否定侧还须与另一句字面重合(字符二元组 Jaccard ≥ 0.40)且语义相近(余弦 ≥ 0.35),两个阈值均可由构造参数(`neg_flip_min_cos` / `neg_flip_min_overlap`)调整。
46
+ - 「不变」「不同」「不管」「不再」「无需」「不可观测」「不可控」这类描述性「不 / 无」不再算作立场否定。
47
+ - 冲突卡新增次按钮「误判」,与「采纳所选观点」并列。
48
+ - 新增 `tools/recheck_conflicts.py`:用当前规则复算存量 open 卡并批量关闭已不成立的,默认 dry-run,`--apply` 经运行中的服务提交,保证进程内冲突集合与向量索引同步。
49
+
7
50
  ## 0.1.3 — 2026-11-11
8
51
 
9
52
  界面双语化,并把「设置」放到该在的位置。
package/python/README.md CHANGED
@@ -43,6 +43,12 @@ Closed loop (`ChannelDispatcher`): receive → Supervisor → reply to origin, w
43
43
 
44
44
  Keeps the system reliable under message redelivery, crashes, long tasks, and loops. See [docs/GOVERNANCE.md](docs/GOVERNANCE.md): idempotency guard, crash/restart recovery (boot_id avoids pid reuse), tool-loop detection, context compaction, hook standardization, steering queue.
45
45
 
46
+ **Content boundaries.** Requests and model output pass a deterministic content gate: narrow built-in rules for request-shaped violations, plus an optional external lexicon for platform categories.
47
+
48
+ **No political, sexual, or violent term list is bundled, and nothing in that area is enforced unless the deployer mounts one.** Whether to mount a lexicon, which categories to take, and whether to run `enforce` or `audit` is entirely a deployment decision — this repository provides the mechanism, not a policy. The gate is normalised string matching, not semantic understanding: it does not guarantee coverage, and it is not a content moderation or compliance service. Deployers who expose this service publicly are responsible for their own content compliance. The software is provided "as is", as stated in [LICENSE](LICENSE).
49
+
50
+ See [docs/CONTENT_GUARD.md](docs/CONTENT_GUARD.md) for mounting, conversion, and false-positive tuning.
51
+
46
52
  ## Quick start
47
53
 
48
54
  ```bash
@@ -59,6 +59,12 @@ Foxclaw: 记忆 = 持续认知运行时,智能体 = 会推理 · 会学
59
59
  - **钩子标准化** —— 标准生命周期钩子 + 明确阻断权
60
60
  - **干预队列** —— 租约机制安全回收子任务结果(为并行子 Agent 预留)
61
61
 
62
+ **内容边界。** 用户请求与模型输出两端各有一道确定性内容闸门:内置规则只管请求型教唆,平台大类词表按需挂载。
63
+
64
+ **仓库不附带任何政治、色情、暴力词表,这块不挂载就完全不生效。** 是否挂载、取哪几类、跑 `enforce` 还是 `audit`,完全由部署方自行决定 —— 这里提供的是机制,不是口径。闸门做的是归一化后的字符串匹配,不是语义理解,不保证覆盖率,也不构成内容审核或合规服务;对外提供服务时的内容合规责任在部署方自身。软件按“现状”提供,详见 [LICENSE](LICENSE)。
65
+
66
+ 挂载方式、格式转换与误伤调优见 [docs/CONTENT_GUARD_CN.md](docs/CONTENT_GUARD_CN.md)。
67
+
62
68
  ## 五、目录结构
63
69
 
64
70
  ```
@@ -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).
@@ -0,0 +1,189 @@
1
+ # 内容闸门与敏感词表
2
+
3
+ 这层只做一件事:在**用户请求**和**模型输出**两端做确定性判定,命中就用一句固定话术切断。
4
+
5
+ 它不调用模型、不做语义理解,也不替代平台审核或人工审核。提示词层负责让模型自己不越线,这一层负责在模型或用户绕过提示词时守住下限。
6
+
7
+ ## 三层判定来源
8
+
9
+ | 来源 | 位置 | 可配置 | 覆盖范围 |
10
+ |---|---|---|---|
11
+ | 内置规则 | `foxclaw/core/content_guard.py` 的 `_BUILTIN_RULES` | 否(只能整体 `off`) | 六类**请求型教唆**:制毒制爆、诈骗洗钱、入侵他人系统、伪造证件、自杀方法、未成年性内容 |
12
+ | 外部词表 | `FOXCLAW_CONTENT_LEXICON` 指向的文件 | 是 | 由你决定,政治 / 色情 / 暴力等大类都在这一层挂 |
13
+ | 提示词 | `KERNEL_PROMPT` 【八、内容边界】 | 改代码 | 软约束,负责语气与拒绝范式 |
14
+
15
+ 内置规则刻意写得很窄——必须同时出现「动作词 + 对象」才算命中,比如「怎么制作炸药」命中,「我买了一本讲炸药发展史的书」不命中。目标是开箱即用且不打断正常对话,不是覆盖所有违规表达。
16
+
17
+ **数万条量级的大类词表不内置**:监管口径会变、误伤面由部署方决定、词表体积也不该压进代码。要不要挂、挂哪几类,是部署决策,交给你。
18
+
19
+ ## 默认行为
20
+
21
+ 装好就生效,不需要配置:
22
+
23
+ - 模式默认 `enforce`
24
+ - 只有内置规则在跑(没挂词表时)
25
+ - 输入命中 → 直接返回固定话术,**请求不进模型**
26
+ - 输出命中 → 整段替换为固定话术,不保留任何原文片段
27
+ - 流式输出命中 → 在命中点停止继续下发
28
+
29
+ 固定话术跟随输入语种,只有中英两种:`我们换个话题聊聊吧。` / `Let's talk about something else.`
30
+
31
+ 不解释、不延伸、不反问、不给替代方案。拒绝本身不能变成信息泄漏或继续纠缠的入口。
32
+
33
+ ## 三档模式
34
+
35
+ ```bash
36
+ FOXCLAW_CONTENT_GUARD_MODE=enforce # 默认:真拦截
37
+ FOXCLAW_CONTENT_GUARD_MODE=audit # 只记录不拦截,用于观察误伤
38
+ FOXCLAW_CONTENT_GUARD_MODE=off # 完全关闭
39
+ ```
40
+
41
+ 写错的值会回退到 `enforce`(宁可多拦,不会静默失效)。
42
+
43
+ `audit` 档的命中记录打在**进程 stdout** 上:
44
+
45
+ ```
46
+ [Foxclaw] ⚠ 内容命中(未拦截): category=politics word='某某'
47
+ ```
48
+
49
+ 只有 `audit` 会打印。`enforce` 档命中是静默的——需要审计留存的话,在调用侧接 `Decision.to_dict()`,或者先跑一段 `audit` 采集基线。
50
+
51
+ ## 配置写在哪
52
+
53
+ 两条路,效果不同:
54
+
55
+ - **`.env`(推荐)**:API 服务在导入包之前加载 `.env`,写进这里即可生效。注意 `.env` 里的空值视为未配置,不会覆盖系统环境变量。
56
+ - **系统环境变量**:SDK 里直接用 `Foxclaw()` 时 `.env` 不会被自动加载,必须在进程启动前设好环境变量。
57
+
58
+ `.env` 加载可以关掉:`FOXCLAW_SKIP_DOTENV=1`。
59
+
60
+ ## 词表格式
61
+
62
+ 一行一条,UTF-8,`#` 开头为注释:
63
+
64
+ ```
65
+ # 词<TAB>类别,类别可省略
66
+ 某词 politics
67
+ 另一个词 sexual
68
+ 不带类别的词
69
+ ```
70
+
71
+ 规则细节(都来自加载器实现,不是约定俗成):
72
+
73
+ | 项 | 行为 |
74
+ |---|---|
75
+ | 分隔符 | `词<TAB>类别`;没有 TAB 时整行是词,类别取 `platform` |
76
+ | 注释 | `#` 开头整行跳过 |
77
+ | 空白与标点 | 每个词先过一遍 `compact()`:去空白、去中英标点、全半角归一、转小写 |
78
+ | 长度 | `compact()` 之后少于 2 个字符的**直接丢弃**(单字词误伤太大,且几乎拦不住真违规) |
79
+ | 同词多类别 | 允许;扫描结果按 `(类别, 词)` 去重,命中取第一个 |
80
+ | 坏行 / 读不到的文件 | 静默跳过,不会让服务起不来 |
81
+ | 生效时机 | **进程启动时建索引,改词表要重启**;没有热重载 |
82
+
83
+ 路径用**绝对路径**最稳。相对路径按进程工作目录解析,systemd 之类的工作目录可能不是项目根。
84
+
85
+ 改完词表重启,在启动后确认加载条数——可以先手工载入一次:
86
+
87
+ ```bash
88
+ python -c "from foxclaw.core.content_guard import ContentGuard; print(ContentGuard().lexicon_size)"
89
+ ```
90
+
91
+ 打印 0 就是没读到。
92
+
93
+ ## 从第三方词库转换
94
+
95
+ 现成的敏感词库(如 `houbb/sensitive-word` 的 6 万条主词库、各类按政治/色情/暴力分好类的版本)格式各不相同:有的单文件纯词,有的一个类别一个文件,有的 GBK 编码,有的同一批词里混着 `敏感词`、`敏 感 词`、`SENSITIVE` 三种写法。
96
+
97
+ `tools/build_content_lexicon.py` 负责转换。**这个脚本在仓库里,不随 wheel 分发**——`pip install` 的用户需要从 GitHub 仓库取:
98
+
99
+ ```bash
100
+ # 单文件主词库,全部归到 platform 类别
101
+ python tools/build_content_lexicon.py sensitive_word_dict.txt -o content_lexicon.txt
102
+
103
+ # 分类词库:文件名含中文就用文件名当类别(政治类.txt → 政治类)
104
+ python tools/build_content_lexicon.py dict/*.txt -o content_lexicon.txt
105
+
106
+ # 自己挑类别:只留需要的那几类
107
+ python tools/build_content_lexicon.py dict/*.txt --exclude 政治类,色情类 --exclude "暴力*" -o content_lexicon.txt
108
+
109
+ # 单文件但想指定类别名
110
+ python tools/build_content_lexicon.py ads.txt --category ads -o content_lexicon.txt
111
+ ```
112
+
113
+ 它做四件事:
114
+
115
+ 1. **自动判编码**:先 `utf-8-sig` 后 `gb18030`。国内下载的词库不少是 GBK,而加载器固定按 UTF-8 读并忽略坏字节——这步不做,词会在挂载后静默缺失。
116
+ 2. **转换期归一化**:用加载器同一个 `compact()`,重复写法当场合并,不让自动机被同一词的三种写法撑大。
117
+ 3. **按类别筛选**:`--include` / `--exclude` 支持通配符,逗号分隔,可重复。
118
+ 4. **回读验证**:转换完用真正的 `ContentGuard` 载入输出文件,抽样扫描确认能命中;不过关直接退出码 1。
119
+
120
+ 常用参数:
121
+
122
+ | 参数 | 默认 | 说明 |
123
+ |---|---|---|
124
+ | `-o, --output` | `content_lexicon.txt` | 输出路径 |
125
+ | `--category` | `platform` | 文件名不含中文时使用的类别名 |
126
+ | `--include` / `--exclude` | 无 | 类别筛选,支持 `通配*` |
127
+ | `--encoding` | `auto` | 可强制指定 |
128
+ | `--min-len` | `2` | 短于该长度丢弃(与加载器一致) |
129
+ | `--max-len` | `0` | 长于该长度丢弃,0 为不限 |
130
+ | `--sample` | `5` | 验证抽样条数 |
131
+ | `--no-verify` | 关 | 跳过载入验证 |
132
+
133
+ 六万条规模的词表,转换加快载入验证在半秒量级。
134
+
135
+ 转换产物已在 `.gitignore` 里排除(`/content_lexicon*.txt`),不要提交进仓库。
136
+
137
+ ## 挂上之前先跑 audit
138
+
139
+ **这是最容易跳过、后果最直接的一步。**
140
+
141
+ 六万条量级的词表里必然有大量双字词。直接挂 `enforce`,正常的订单讨论、书稿审阅、医学问答都可能被切断——命中就整段替换,用户不知道发生了什么,只知道助手突然开始说「我们换个话题聊聊吧。」
142
+
143
+ 建议流程:
144
+
145
+ 1. 挂词表,`FOXCLAW_CONTENT_GUARD_MODE=audit`
146
+ 2. 正常用几天,收集 stdout 里的命中行
147
+ 3. 逐条看 `category` 和 `word`:哪些是该拦的,哪些是误伤
148
+ 4. 误伤多的类别用 `--exclude` 摘掉,或者换更长的词条重新生成
149
+ 5. 确认可接受后再转 `enforce`
150
+
151
+ 没有白名单机制。压制误伤的手段只有三个:少挂类别、用更长的词条、把 `--min-len` 调高。
152
+
153
+ ## 精度说明
154
+
155
+ 这一层是**限速带**,不是墙。明确不处理:
156
+
157
+ - 拼音、谐音、拆字、外文转写(追下去是无底洞,且误伤率更高)
158
+ - 图片、语音、附件正文
159
+ - 跨轮诱导(把一句话拆成三轮说)
160
+ - 知识库导入的书籍、法律条文、医学教材内容
161
+
162
+ 最后一条是刻意的:**过滤器只挂在「用户请求」与「模型输出」两端**,不碰用户自己的资料。历史书里写「炸药配方」不该导致这本书导入失败。
163
+
164
+ 流式输出有个固有限制:命中那一刻只能保证之后不再吐字,**已经下发的前缀收不回来**。非流式路径是整段替换,没有这个问题。
165
+
166
+ ## 排查清单
167
+
168
+ | 现象 | 检查 |
169
+ |---|---|
170
+ | 完全不拦 | `FOXCLAW_CONTENT_GUARD_MODE` 是不是 `off`;环境变量有没有真的进进程 |
171
+ | 内置规则生效、词表不生效 | `ContentGuard().lexicon_size` 是否为 0;路径是相对还是绝对;文件编码 |
172
+ | 挂了词表后某些词没拦到 | 词是不是 `compact()` 后不足 2 字被丢了;词表里是不是有空格被当成了词的一部分 |
173
+ | 改了词表没变化 | 进程没重启;索引是启动时建的 |
174
+ | 正常对话被切断 | 词表里双字词太多,先降 `audit`,或摘掉该类 |
175
+
176
+ ## 和提示词层的关系
177
+
178
+ 两层职责不同,都要有:
179
+
180
+ - 提示词层(`KERNEL_PROMPT` 【八】+ `behavior.yaml` 的 `no_harmful_content`)负责语义边界:什么算违规、拒绝时怎么说话、用户的资料和指令怎么区分。
181
+ - 代码层(本文件)负责确定性下限:词表命中就是命中,不经过模型判断。
182
+
183
+ 只做提示词,模型被绕过去就没有兜底;只做代码,边界场景的取舍全靠词表,回答会变得机械。两层一起,才是「模型自己知道分寸,同时越线时有保险」。
184
+
185
+ ## 责任边界
186
+
187
+ 仓库不附带任何政治 / 色情 / 暴力词表。是否挂载、挂哪几类、跑哪一档,全部由部署方决定,项目不预设口径。
188
+
189
+ 这一层做的是归一化后的字符串匹配,不保证覆盖率,也不构成内容审核或合规服务。对外提供服务时的内容合规责任在部署方自身。软件按“现状”提供,详见 [LICENSE](../LICENSE)。