钩子 — 安全与质量
输出护栏与 DLP:检查最终回答并保护数据。所有钩子的列表见 06-hooks-reference。
6.9 link_check
输出护栏:检查最终回答中的链接,并标记那些无法被来源证实(grounded)或已失效(live)的链接。接地依据 RAG 代理的归档(与 rag/archive_tool 相同的 archive::open_cached);结构性检查(绝对 http(s)、公共主机)始终执行。捕获模型编造的 URL——这是代理的主要失败点,而 RAG 指令要求引用来源。
kind: link_check
name: link_guard
mode: grounded # grounded | well_formed | live | both
archive_path: data/ngu # источник заземления (пусто = только структура)
check_bare_urls: true # «голые» http(s):// вне markdown
check_html_anchors: false # HTML <a href>
max_links: 20 # больше — нейтрально
on_invalid: notice # notice | annotate | unlink | rework
notice_template: "⚠ {{count}} ссылк(и) не подтверждены источниками"
annotate_template: "{{title}} ⚠"
rework_template: "" # для on_invalid: rework; {{links}}, {{count}}
rework_clear_stream: true # false = не очищать уже отданный стрим (видимая самоправка / клиент без replace)
# live-режим (обычно только на администраторском глобальном хуке):
allow_hosts: [] # пусто = любой; иначе leaf/example.com + поддомены
timeout_ms: 2000
cache_ttl_secs: 86400 # 0 = без кэша
max_body_bytes: 0 # 0 = HEAD; >0 = GET (тело не читается)
| 字段 | 描述 |
|---|---|
mode |
grounded — 依据归档;well_formed — 仅结构;live — 结构 + HTTP 存活;both = grounded + live |
archive_path |
归档目录;为空/缺失 → grounded 检查关闭(中性) |
check_bare_urls / check_html_anchors |
扫描哪些形式的链接 |
max_links |
链接过多 — 钩子中性(不检查) |
on_invalid |
notice — 通知(默认);annotate — 链接文本后缀;unlink — 链接 → 文本;rework — 拒绝回答并强制该轮继续(见 §6.1 rework) |
notice_template / annotate_template / rework_template |
{{count}} / {{title}} / {{links}} |
rework_clear_stream |
在 rework 时清除已发出的回答(replace)。false — 保留流(可见的自我修正或无 replace 的客户端) |
allow_hosts |
live 的主机 allowlist(空 = 任意公共主机);列表外的主机不查询(不标记) |
timeout_ms / cache_ttl_secs / max_body_bytes |
超时、存活缓存 TTL、方法(HEAD/GET) |
「有效」= 已接地(URL 存在于归档中;允许尾部 / 和 #…)、结构安全,并且在 live 下存活。live 不跟随重定向(3xx = 存活)——以此避免通过重定向的 SSRF;401/403/405/406/429 视为可达(反机器人),超时 — 「未知」(不标记),而 404/410 和网络错误 — 视为失效。检查并行进行,并按 URL 缓存在内存中。任何错误(无归档、无匹配、链接过多)时钩子中性——该轮不会中断。
annotate / unlink 会修改回答,并依赖重发契约(replace);仅在能够替换已显示回答的客户端上安全。默认 — notice。
rework 不修改回答,而是拒绝它:内核清除已发出的流(best-effort,replace: true)并注入一条带有未证实链接列表的 correction 消息,之后模型重新回答——代理在同一轮内修正链接。上限 MAX_REWORK_ROUNDS(2)保证终止:如果模型无法完成,该轮以最后一个回答结束(降级,不循环)。
6.12 format_guard
在 after_assistant_message 上确定性地整理最终回答(不使用 LLM):使链接可点击并移除「裸」URL。钩子就地修改回答,内核重发修正后的版本;如果无需修改 — 中性。
kind: format_guard
name: format_guard
linkify_bare_urls: true # https://… → [https://…](https://…)
dedupe_links: true # дубли ссылок (по target) схлопываются
one_link_per_line: true # каждая ссылка с новой строки
stream: true # форматировать и поток токенов (on_token)
notice: "" # уведомление в UI при изменении (пусто = тихо)
| 字段 | 描述 |
|---|---|
linkify_bare_urls |
将裸 http(s)-URL 包裹为 markdown 链接(已格式化的不动) |
dedupe_links |
按 target 的重复项替换为其文本 |
one_link_per_line |
如果一行中有多个链接 — 分行排列 |
stream |
格式化 on_token:URL 被缓冲并包裹,客户端立即看到修正后的文本 |
notice |
如果回答被修改,显示 UI 通知 |
流。 after_assistant_message 修改已保存/已发出的回答,内核重新发送完整的 final_answer(重发)——对流式客户端来说这是多余的一帧。在 stream: true 下,链接直接在流中格式化(on_token):安全字符立即输出,而 URL 缓冲到终止符,然后作为 [url](url) 插入。after_assistant_message 仍是权威的最终处理(去重、one-per-line、非流式通道);重复应用是幂等的。
6.13 guard
无状态的统一安全膜。用通用引擎 agent_os_core::safety::scanner 在模型的四个边界上扫描文本,并按实体类别应用动作:
| 边界 | 点 | 覆盖内容 |
|---|---|---|
| Ingress(进入模型) | before_inference |
messages + tool 模式(+ rag/memory 注入) |
| Egress(面向用户) | after_assistant_message |
最终回答 |
| Tool-args(来自模型) | before_tool_call |
调用参数 |
| Tool-result(进入模型) | after_tool_result |
工具结果 |
动作:audit_only · notice · redact(不可逆掩码 [REDACTED])· deny(拒绝)· suppress · rework(重写回答)。tokenize 在 guard 中不可用——请使用 pseudonymize。当类别混合时,最严格的动作胜出。
kind: guard
name: dlp
direction: both # input | output | tool | both (input+output) | all
on_input: redact # audit_only | notice | redact | deny
on_output: rework # audit_only | notice | redact | suppress | deny | rework
# on_tool_args: redact # по умолчанию = on_input
# on_tool_result: redact
entities: [email, phone, card, iban, passport, inn, snils, api_key, pem]
actions: # переопределение действия на класс
api_key: deny
allow: ["support@company.com"]
patterns: # свой regex на класс (напр. person)
person: '\b[А-ЯЁ][а-яё]+\s[А-ЯЁ][а-яё]+\b'
detector: deterministic # deterministic | llm (принимается; в v1 = deterministic)
audit_only: false # true = только считать, не менять
notice_template: "⚠ Скрыто {{count}} чувствительных данных"
rework_template: "Убери эти данные и ответь заново: {{kinds}}"
rework_clear_stream: true
| 字段 | 描述 |
|---|---|
direction |
哪些边界处于活动状态 |
on_input / on_output / on_tool_args / on_tool_result |
边界的默认动作 |
entities |
实体类别;空 = 所有内置类别 |
actions |
针对特定类别覆盖动作 |
allow |
不标记的值/子串 |
patterns |
针对类别的额外 regex(person、org、……) |
ner_model |
此钩子的 ONNX-NER 模型 id(per-agent);空 = 按代理模板选择器 → default |
audit_only |
计数但不修改文本 |
notice_template |
{{count}} — 触发次数 |
rework_template |
{{kinds}}、{{count}} — 给模型的修正文本 |
| Fail-open。 扫描出错时保持文本原样。值不会被记录——只记录类型和计数。 |
NER。 detector: ner / hybrid 通过内核的通用 ONNX 服务,用本地 ONNX 模型(类别在 ner_classes 中,例如 [person, org, address])补充规则;没有该服务时,钩子仍依赖规则。模型选择——ner_model(per-agent hook YAML),否则按 agent 模板选择器(templates),然后是 default。
6.14 pseudonymize
在同一个扫描器之上进行代理值的可逆替换。模型只能看到 [TYPE_n],真实值本地存储在 DuckDB-vault 中,并在输出时展开。
kind: pseudonymize
name: pii_vault
scope: session # session | user | template
entities: [person, email, phone, address, org, passport]
detokenize_output: true # разворачивать ответ пользователю (on_token)
detokenize_tool_args: true # side-effect'ы идут с реальными значениями
tokenize_tool_results: true
fail_closed: true # не отправлять, если суррогат не удалось поставить
vault_path: data/pii_vault.db
placeholder_format: "[{kind}_{n}]"
output_dlp: true # страховка: маскировать сырое PII в ответе
patterns: {} # свой regex на класс (person/org требуют его)
allow: []
Vault 表(data/pii_vault.db):
CREATE TABLE pii_map (
scope_key TEXT NOT NULL,
placeholder TEXT NOT NULL,
kind TEXT NOT NULL,
value TEXT NOT NULL,
created_at BIGINT NOT NULL,
last_used BIGINT NOT NULL,
PRIMARY KEY (scope_key, placeholder)
);
CREATE INDEX idx_pii_value ON pii_map(scope_key, kind, value);
- 一个实体 → 一个占位符(按
(scope, kind, value));编号全局唯一,因此其他作用域的占位符会“未知”。 scope: session(默认)——密钥随会话消亡;user——跨会话一致性;template——按模板共享。on_token以缓冲方式对流转进行去令牌化(即使占位符在分块之间被拆开,仍会展开)。上下文/转录保持为代理值——原始 PII 不会进入模型,也不会进入保存的响应。fail_closed:vault/检测器不可用 → 推理被拒绝(deny),而不是发送原始数据。detector: ner/hybrid+ner_classes——通过内核的 ONNX 服务,用本地 ONNX 模型补充规则。ner_model——选择具体的 ONNX-NER 模型(per-agent);否则按 agent 模板选择模型(templates),然后是 default。ttl_secs/max_entries——vault 保留策略(按last_used清除、eviction)。scope: session——会话关闭时清除(on_session_closed);scope: user——合并用户时迁移密钥(merge_user)。vault_key_env/vault_key——vault 加密密钥:给定密钥时,列value使用 AES-256-GCM 加密,查找/去重按 HMAC 哈希进行(value_hash);没有密钥时——plaintext(并给出警告)。GET /v1/pii/vault/:scope(admin)——针对某个作用域的 subject-access 导出,导出解密后的 vault 记录。
假名化 ≠ 匿名化:数据仍然是个人数据,且可重新识别;vault 不会离开边界。
6.16 groundedness
after_assistant_message 上的输出护栏:将最终回答拆分为断言(句子),并针对可用的 evidence 逐一检查——断言中在 evidence 里找到的实义 token 占比不得低于 min_support。未确认的断言以通知形式显示;回答不会改变,也不会被抑制。它能抓住 link_check 漏掉的情况:引用真实,但断言是编造的。确定性,不使用 LLM 和网络。
Evidence(evidence)——RAG agent 的归档(对每个断言进行 BM25 搜索)以及会话中成功的工具结果(ngu_search、http_fetch、……——即 agent 实际获得的内容)。
kind: groundedness
name: grounding_guard
archive_path: data/ngu # источник evidence (общий archive::open_cached)
evidence: auto # auto | archive | tools | both (both == auto)
tool_results_limit: 32 # последних tool-результатов сессии в пул
max_evidence_chars: 200000 # потолок текста evidence из инструментов
min_support: 0.5 # доля токенов claim, найденных в evidence
min_claim_chars: 24 # короче — не claim
min_claim_tokens: 3 # меньше содержательных токенов — не claim
max_claims: 30 # больше — не проверять (нейтрально)
top_chunks: 3 # сколько чанков архива брать на claim
skip_questions: true # пропускать вопросительные предложения
on_invalid: notice # notice | rework
rework_template: "Эти утверждения не подтверждаются источниками: {{claims}}. Убери их или подтверди и ответь заново."
rework_clear_stream: true # для rework: очистить уже отданный стрим (replace)
notice_template: "⚠ {{count}} утверждени(й) не подтверждается источниками: {{claims}}"
max_notice_claims: 3 # сколько claim показать в notice
| 字段 | 描述 |
|---|---|
archive_path |
归档目录(evidence);为空 → 归档来源关闭 |
evidence |
auto/both——归档 + 工具;archive——仅归档;tools——仅工具 |
tool_results_limit |
考虑会话中最近多少个工具结果 |
max_evidence_chars |
来自工具的证据文本总量上限 |
min_support |
claim 的 token 被 evidence 覆盖的阈值(0..1) |
min_claim_chars / min_claim_tokens |
过滤过短/非事实性句子 |
max_claims |
更大——钩子不检查(中性) |
top_chunks |
每个断言取多少个归档分块 |
skip_questions |
不将问题视为 claim |
on_invalid |
notice——通知(默认);rework——拒绝回答并继续该轮次({{count}}、{{claims}},见 §6.1) |
rework_template / rework_clear_stream |
correction 消息模板以及针对 rework 的流清理 |
notice_template |
{{count}}(数字)和 {{claims}}(前几个未确认项) |
max_notice_claims |
通知文本中的断言数量上限 |
URL 和 markdown 标记会在分词前从断言中剔除(markdown 链接 [текст](url) → текст)。归档只打开一次并缓存(与 archive::open_cached 共享 rag/link_check/archive_tool);工具结果取自 HookContext.tool_results(ToolResultLookup::recent_all,in-memory per-session,仅成功项)。如果没有任何来源可用,则为中性。改写和综合可能被标记为未确认——因此默认 notice;rework 请在自己的语料上校准阈值后再启用。与 link_check 一起使用(钩子相互独立)。
6.18 language_guard
确保最终回答以预期语言书写——用户的语言(target: auto)或指定语言。由三个点组成的混合方案:在用户消息上捕获语言(before_user_message)、可选的预防性指令(before_inference),以及最终回答的 guard(after_assistant_message)。
关键点:after_assistant_message 只在最终(tool-free)回答上触发,而那时用户消息已经不在了——因此必须提前捕获用户语言(in-memory 按进程,在 on_terminate/on_session_closed 时清理)。
检测。 基于脚本、确定性、不使用 LLM:字母按 Unicode 范围分类(Latin、Cyrillic、Greek、Arabic、Hebrew、Devanagari、Han、Hangul、Thai),取占主导的组。能区分 ru↔en,但不能区分 en/es/de——对于同一脚本的语言,会启用细化 tier(detector):
detector |
做什么 | 代价 |
|---|---|---|
script |
仅脚本(默认) | 零 |
ngram |
whatlang trigram 检测器,进程内 |
零,无模型/网络 |
onnx |
通过内核 ONNX 服务的微型 language-id 模型(onnx_model,默认 langid;自动加载) |
本地推理 |
llm |
子进程评判器 | token |
当 min_ratio 低于阈值时,响应被视为混合,不标记;
短响应(min_chars)不检查。
模型 langid(mini-BERT,约 25 MB int8,200 种语言)已注册在
config/server.yaml → security.onnx.models,并在首次使用时自动下载
(download: auto,仓库 onnx-community/language_detection-ONNX,固定修订版)。需要
带 onnx 特性的服务器构建。如果
ONNX 服务或模型不可用,该钩子保持中立——回合不会中断。
哪些语言算「ok」。 allowed(代码/脚本列表)比 target
更强(auto = 用户语言)。区分同一脚本内的语言
(en/es,ru/uk)只能借助 detector: ngram/onnx/llm;在
detector: script 下,允许同一文字家族的任意响应。
kind: language_guard
name: language_guard
target: auto # auto | <код: ru, en, …> | <скрипт: cyrillic, latin, …>
allowed: [] # непусто = ответ обязан быть в одном из списка
min_chars: 40 # короче — не проверяем
min_ratio: 0.5 # доля доминирующего скрипта; ниже — смешанный текст
sample_chars: 400 # образец сообщения пользователя для tier'а
on_mismatch: notice # notice | rework
notice_template: "⚠️ Ответ не на языке запроса (ожидался {{expected}}, получен {{detected}})"
rework_template: "" # пусто = встроенный текст; {{expected}}, {{detected}}
rework_clear_stream: true
detector: script # script | ngram | onnx | llm
onnx_model: langid # id модели для detector: onnx (см. security.onnx.models)
model: "" # дешёвая модель для detector: llm
min_confidence: 0.6
judge_instructions: "" # свой prompt судьи (JSON-ответ)
inject_directive: false # превентивно на before_inference
directive_template: "Always answer in {{language}}."
| 字段 | 描述 |
|---|---|
target |
auto — 跟随用户;否则为代码(ru)或脚本(cyrillic) |
allowed |
语言/脚本的 allowlist;比 target 更强 |
min_chars / min_ratio |
响应长度阈值和脚本检测置信度阈值 |
sample_chars |
将用户消息的多少个字符交给 tier |
on_mismatch |
notice(默认)或 rework(在同一回合内重写) |
notice_template / rework_template |
{{expected}},{{detected}} |
rework_clear_stream |
在 rework 时是否清除已发出的流 |
detector |
script / ngram / onnx / llm(见表格) |
onnx_model |
用于 detector: onnx 的 language-id 模型 id(默认 langid) |
model / min_confidence / judge_instructions |
LLM 评判器(detector: llm) |
inject_directive / directive_template |
「用……回答」指令({{language}}) |
结果。 匹配 / 无可检查 → 中立。不一致 →
notice 或 rework(内核清除流并注入 system 修正,上限
MAX_REWORK_ROUNDS)。replace/suppress 不使用:无法
确定性地翻译文本(见 §6.1 rework)。指令是幂等的(标记)。
6.28 schema_guard
根据 JSON Schema 验证结构化输出,在不匹配时要么
要求模型修正(rework),要么标记(notice)。补充
流程的 output_schema(后者只检查 job_done-payload):该钩子
在每个最终响应上触发。
触发点:
after_assistant_message(target: answer)——验证响应;on_invalid: notice | rework。before_tool_call(target: tool_args)——验证工具参数;on_invalid: deny | notice(deny返回 failed 结果,模型看到 错误并可重试调用)。
kind: schema_guard
name: schema_guard
target: answer # answer | tool_args
on_invalid: rework # answer: notice | rework ; tool_args: deny | notice
schema:
type: object
required: [answer]
properties:
answer: { type: string }
# notice_template: "⚠ Схема не соблюдена: {{errors}}"
# rework_template: "Ответ не соответствует схеме: {{errors}}"
# tools: [structured_search] # tool_args: ограничить список
# exempt_tools: [] # tool_args: исключить
| 字段 | 描述 |
|---|---|
target |
answer(默认)或 tool_args |
schema |
用于验证的 JSON Schema |
on_invalid |
answer:notice(默认)/ rework;tool_args:deny(默认)/ notice |
notice_template / rework_template |
{{errors}}(以及用于 notice 的 {{tool}}) |
tools / exempt_tools |
tool_args:allowlist / 工具排除项 |
对于 answer,JSON 甚至可以从 fenced 块(```json … ```)或
包含 JSON 对象/数组的文本中提取。验证器与 output_schema
相同(agent_os_core::json_schema)。