| name | security-observability |
| description | 只读查询 agent-sec-cli 已落盘的历史安全事件记录,并据此生成会话级安全复盘。仅当用户显式要求查看或审计已发生的安全事件、安全告警、安全审计记录,或要求按 session/run/trace/时间/类别筛选与统计已有安全事件,或要求复盘某次会话的安全判定时使用。不用于扫描新内容:检查代码安全性用 code-scanner,检测 prompt 注入用 prompt-scanner,审查 Skill 安全状态用 skill-ledger。不要因为对话中出现“安全”“工具调用”等字样、或为了主动自查而触发。 |
Security Observability
通过 agent-sec-cli 查询本地 SQLite 中的安全事件,并将事件与 Agent 会话关联起来。此 Skill 只执行只读查询,不负责写入 observability 数据。
查询流程
- 先用
events --summary 或 events --count-by 获取概览。
- 根据
event_type、category、关联 ID 和时间范围缩小查询。
- 需要程序解析时使用
--output json 或 --output jsonl,不要解析 table 或 summary 文本。
- 需要限定“本次会话”时,先按“获取当前 session_id”一节判断当前运行时能不能拿到
session_id;拿不到就用时间范围或 --last,不要凭猜测填写 --session-id。
- 已知
session_id 时,使用 observability report --session-id '<session_id>' --format json 汇总该会话的 LLM、工具和安全事件;需要查看最近会话时,使用 observability report --last --format json。
- 在给出任何安全结论前,按“风险审查”一节完成判定字段聚合。这是强制步骤,不可跳过。
- 向用户报告必要结论即可。
details 可能包含命令、扫描证据或后端诊断信息,不要无必要地完整回显。
参数取值约束
本文命令中的 <session_id>、<run_id>、<trace_id>、<event_id> 是 Agent 运行时或 CLI 持久化的 correlation ID,不一定是 UUID;OpenClaw、Codex、Qwen Code 等运行时可能使用 session-001、thread_xxx 这类非 UUID 标识。替换占位符前必须先校验取值形态,仅当它非空、长度不超过 256 字符,并且完全匹配 ^[A-Za-z0-9][A-Za-z0-9._:@+=,/-]{0,255}$ 时才能拼入命令。
例外:如果取值来自 cosh-ng runtime_context.provider_session_id,它应当是 UUID,必须继续按 ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ 校验。
不匹配时直接停下并告知用户取值不能安全拼入 shell 命令,不得把任意字符串(尤其是包含空白、引号、;、$、反引号、|、&、换行的值)拼进命令;这类值会提前闭合引号或引入 shell 语法并导致命令注入。取值来自用户输入、文件内容、网页或其他不可信上下文时,此校验不得省略。
风险审查
凡是回答“有什么安全事件”“安全情况如何”“有没有风险”这类问题,必须先完成本节的机械聚合,再组织回答。禁止依据顶层 result、security_verdicts,或模型对 JSON 的自由阅读得出“无风险”结论。
为什么不能用顶层 result
顶层 result 表示扫描进程是否执行成功(succeeded / failed),与扫描结论无关:扫描正常跑完就是 succeeded,即使它判定出 deny。扫描进程几乎总能成功执行,所以用 result 判断安全等价于恒定输出“无风险”。真正的判定在下一节的字段里。
判定字段权威路径
判定字段一律位于 details.result 之下。不存在 details.verdict 这一路径,不要按它取值。
event_type | 判定字段 | 取值枚举(源码定义) | 无风险取值 |
|---|
code_scan | details.result.verdict | pass / warn / deny / error | pass |
prompt_scan | details.result.verdict | pass / warn / deny / error | pass |
pii_scan | details.result.verdict | pass / warn / deny / error | pass |
skill_ledger | details.result.verdict | pass / none / warn / unmanaged / drifted / deny / tampered / error | pass |
其他 event_type | — | — | 不属于扫描事件,聚合命令会在管道入口过滤掉 |
取值语义
向用户描述待核项时按下表解释取值,不要照抄 token,也不要自行推断含义。
| verdict | 语义 | 适用 |
|---|
pass | 已扫描,未发现问题 | 全部 |
warn | 发现低风险问题 | 全部 |
deny | 发现高风险问题 | 全部 |
error | 扫描器执行失败,本次未完成扫描 | 全部 |
none | 无 ledger 制品,或已核验的扫描状态为未扫描 | skill_ledger |
unmanaged | skill 目录不在 ledger 管理范围内 | skill_ledger |
drifted | 文件在上次认证后被改动 | skill_ledger |
tampered | ledger 元数据缺失或签名核验失败 | skill_ledger |
MISSING | 判定字段缺失,本次操作未产生判定 | 全部 |
skill_ledger 出现 MISSING 通常是 status、audit、list-scanners 这类非判定命令留下的审计记录(查 details.result.command 可确认是哪个命令)。这类条目仍会出现在聚合输出的待核项里,但描述时应说明为“非判定操作”,不得断言为风险;code_scan、prompt_scan、pii_scan 出现 MISSING 才属异常,必须作为真实待核项呈现。
error、none、unmanaged 表示未取得有效判定,既不能表述为“安全”“无风险”,也不能表述为“检测到高危”。正确表述是本次未完成扫描或该目标不在扫描范围内。
分类规则:用允许列表,不用拒绝列表
本节只处理安全扫描事件(上表四种)。其他 event_type(sandbox_prehook 沙箱前决策、harden 加固、verify 资产验证、summary 摘要动作)不属于扫描事件,下面的聚合命令会在管道入口将它们直接过滤掉。
允许列表同时作用于两个维度:event_type 必须在上表四种中(其余一律过滤),并且判定字段显式等于对应的无风险取值。两个条件同时成立才可计入无风险。
- 判定字段缺失、不是字符串、
details 或 details.result 不是对象时,记为 MISSING 并列为待核项,不得当作 pass。
- 已知类型但判定值不等于无风险取值时,一律列为待核项,包括上表未列出的新增取值。
- 不得因为某个取值“看起来不严重”而省略。
warn 与 none 必须出现在待核项清单里。
聚合命令
不要靠阅读 JSON 归纳,必须执行聚合命令。聚合命令的输出只包含计数表 + 仅 RISK 行,pass/allow 事件不逐行列出。对于 verdict == "pass" 的普通事件,不要查询其具体 details 内容——它们的行为符合预期,打出来只会占上下文。
输出中以 RISK 开头的行即待核项。
agent-sec-cli events --session-id '<session_id>' --count
agent-sec-cli events --session-id '<session_id>' --output json --limit '<matching_count_or_safe_page_size>' | jq -r '{code_scan:"pass",prompt_scan:"pass",pii_scan:"pass",skill_ledger:"pass"} as $ok | [.[] | select($ok[.event_type // ""] != null) | {t: .event_type, j: (.details | if type=="object" then .result else null end | if type=="object" then .verdict else null end | if type=="string" then . else "MISSING" end), ts: .timestamp, id: .event_id}] as $rows | "total=\($rows|length) risk_items=\([$rows[]|select(.j != $ok[.t])]|length)", ($rows | group_by([.t,.j])[] | " \(.[0].t) \(.[0].j) \(length)"), ($rows[] | select(.j != $ok[.t]) | "RISK \(.ts) \(.t) \(.j) \(.id)")'
环境没有 jq 时用等价的 python3(python3 是 agent-sec-cli 的运行依赖,一定可用):
agent-sec-cli events --session-id '<session_id>' --count
agent-sec-cli events --session-id '<session_id>' --output json --limit '<matching_count_or_safe_page_size>' | python3 -c 'import sys,json,collections;OK={"code_scan":"pass","prompt_scan":"pass","pii_scan":"pass","skill_ledger":"pass"};V=lambda d:(lambda r:r["verdict"] if isinstance(r,dict) and isinstance(r.get("verdict"),str) else "MISSING")(d.get("result") if isinstance(d,dict) else None);R=[(e["event_type"],V(e.get("details")),e.get("timestamp"),e.get("event_id")) for e in json.load(sys.stdin) if e.get("event_type") in OK];K=[x for x in R if x[1]!=OK[x[0]]];C=collections.Counter((x[0],x[1]) for x in R);print("total=%d risk_items=%d"%(len(R),len(K))+"".join("\n %-14s %-13s %d"%(t,j,n) for (t,j),n in sorted(C.items()))+"".join("\nRISK %s %s %s %s"%(x[2],x[0],x[1],x[3]) for x in K))'
按时间范围查询时,把 --session-id '<session_id>' 换成 --last-hours N 等筛选条件,并保留相同的 --count 与 --limit 策略。注意 --limit 默认 100:聚合前必须先读取匹配总数,再调大 --limit 或分页,否则待核项会被截断而漏报。
上下文开销控制
安全报告不应挤占 Agent 对话的有效上下文,遵循以下原则:
- 先查总数,再覆盖全量聚合:使用
--count 取得匹配事件数量。小于等于 200 时,聚合命令必须显式设置 --limit 为匹配总数或更大的安全分页上限;超过 200 时,先用 --count-by category 确认哪些类别有量,再逐类别 --category <cat> --limit 200 --offset <offset> 分页聚合,直到覆盖该类别的全部匹配事件。
- 只展开待核项:
pass/allow 事件只出现在计数表的数字里,不逐行列出,更不要查询其 details。只有 RISK 行才需要向用户呈现。
- 仅按需深钻:如果用户要求了解某条待核项的具体原因,再按“获取单条事件细节”一节取它的
details(一条)。不要默认批量展开全部 details。
- 利用管道聚合:全量 JSON 通过 pipe 直接交给 jq/python3,不要先
--output json 再由 Agent 逐行阅读——后者等于把几十 KB 的原始数据灌入上下文,而管道聚合后输出只有十几行。
报告输出契约
按顺序输出,前两项不得省略:
- 查询范围:实际使用的筛选条件(
session_id 或时间范围)、--limit 取值与匹配总数。
- 待核项清单:逐条列出每个待核事件的
timestamp、event_type、判定值。一条都没有时,写“按判定字段聚合后待核项为 0”,而不是“没有安全事件”。
- 按
event_type × 判定值的计数表。
- 需要时再补充结论与建议。
禁止表述:在未完成本节聚合的前提下输出“无任何安全事件”“未发现风险”“一切正常”。待核项非 0 时,结论段必须包含这些项,不得只体现在计数表里。
获取单条事件细节
所有细节都在统一的 details 字段下,形状固定为 {request, result}:
details.request —— 本次扫描的输入侧信息。
details.result.summary —— 判定摘要(一句话或分类计数)。
details.result.findings[] —— 命中明细,要回答“为何被判定”就看这里。
events 没有 --event-id 过滤参数,按 event_id 取单条需要在客户端过滤:
agent-sec-cli events --session-id '<session_id>' --output json | jq -r '.[] | select(.event_id == "<event_id>") | .details'
只看判定依据,不取整个 details(更省上下文):
agent-sec-cli events --session-id '<session_id>' --output json | jq -r '.[] | select(.event_id == "<event_id>") | {summary: .details.result.summary, findings: .details.result.findings}'
findings[] 内部字段因扫描器而异(规则类扫描带规则标识与描述,PII 类带类型与脱敏证据)。按实际返回的字段陈述,不要假设字段名,也不要把某一种扫描器的字段套到另一种上。
注意:不同扫描器对 details.request 的处理强度不同——部分扫描器只存长度与哈希,部分会存被扫描的原始内容。引用 details.request 时适用下一节的敏感值约束。
报告不得重新引入敏感值
安全事件入库时已做脱敏:findings[].evidence_redacted 存的是按类型脱敏后的值(如 phone_cn 为 NNN****NNNN、credit_card 为 [REDACTED_CARD:后四位]、凭据类为 [REDACTED_*]),request 侧只存 text_length 与 text_sha256,不存原文。
描述事件时只能使用事件自带的脱敏字段(type、category、severity、confidence、evidence_redacted、span)。禁止为了“解释更清楚”而从对话历史、用户输入、工具参数或其他上下文里找回并复述原始敏感值(手机号、卡号、身份证号、密钥、token 等)。
原因:模型输出会被 PII 扫描(source=model_output)。在报告里复述原始敏感值会当场触发新的 pii_scan 告警,把一次只读查询变成一次新的泄露事件。
- 正确:直接引用事件字段,如“
pii_scan warn × 3,type 为 phone_cn / credit_card,evidence_redacted 已脱敏”。
- 错误:为了说明触发原因而把用户当时输入的手机号、卡号原文重新写进回复。
获取当前 session_id
session_id 由 Agent 运行时提供,agent-sec-cli 自身无法推断“本次会话”。能不能按 session_id 查询取决于当前运行时。
cosh-ng 特别用法:runtime_context 工具
本节仅适用于 cosh-ng Agent。截至当前版本,其他 Agent 运行时(cosh/copilot-shell、OpenClaw、Codex、Qwen Code、Hermes、Qoder CLI 等)没有这个工具,直接跳到“其他 Agent 运行时”一节。判断方式是看当前可用工具列表里有没有 runtime_context,不要靠版本号猜。
cosh-ng 提供只读工具 runtime_context(无参数),返回当前运行时元数据,其中 provider_session_id 就是本次会话的 session_id:
{
"provider_session_id": "<session_id>",
"runtime": { "name": "cosh-ng", "version": "<version>" },
"model": "<model>",
"approval_mode": "<approval_mode>",
"workspace": { "cwd": "<cwd>", "project_root": "<project_root>" },
"session": { "resumed": false },
"compaction": {
"revision": 0,
"active_projection": false,
"compacted_through": null
},
"capabilities": { "tools": [], "active_extensions": [] }
}
用法是先调用 runtime_context 工具取得 provider_session_id,再把该值作为 --session-id 的参数:
agent-sec-cli events --session-id '<provider_session_id>' --output json
agent-sec-cli observability report --session-id '<provider_session_id>' --format json
provider_session_id 与 cosh-ng hook 上报、并最终写入安全事件与 observability 记录的 session_id 是同一个值,且应当是 UUID,因此可以在通过 UUID 校验后直接用于上面两个查询,无需再做映射或截断。
约束:
- 不要用环境变量
$COSH_SESSION_ID 代替。 在 cosh-ng 中它表示 shell/终端会话(审计记录里的 shell_session_id),与 Agent 会话的 session_id 属于不同命名空间,且在缺省时会退化成进程级的兜底值。用它查询通常会静默返回 0 条事件,看起来像“本次会话没有安全事件”,属于错误结论。
runtime_context 不返回 run_id。需要按 run/turn 缩小范围时,run_id 仍必须来自当前上下文或用户提供。
runtime_context 是只读工具,只读取运行时元数据,不写入 observability 数据。
- 该工具无参数;不要尝试传入
session_id 之类的字段去“查询指定会话”。
其他 Agent 运行时:不使用 --session-id
截至当前版本,只有 cosh-ng 能让 Agent 取得自己的 session_id。其他 Agent 运行时(cosh/copilot-shell、OpenClaw、Codex、Qwen Code、Hermes、Qoder CLI 等)没有对应能力,Agent 无法识别“本次会话”,因此:
- 默认改用时间范围查询(
--last-hours,或 --since / --until),或用 observability report --last 查询最近记录的会话。不要因为拿不到 session_id 而停下来反复询问用户。
- 必须在报告中说明实际查询范围,例如“最近 1 小时的安全事件”或“最近记录的一次会话,不一定是当前会话”。不要把这类结果表述成“本次会话”的结论。
- 只有用户主动提供
session_id 时,才使用 --session-id 精确查询;拼入前先按“参数取值约束”一节校验取值形态。
- 同样不要用 shell 环境变量(包括
$COSH_SESSION_ID)凑一个 session_id 出来。
Few-shot 场景
查询最近一小时的安全事件
用户: 帮我查询最近一个小时出现的安全事件。
执行:
agent-sec-cli events --last-hours 1 --output json