sentinel-harness · v0.5.1 · 从零到精通

会构建 agent 的 agent
—— 而且安全。

一个面向 Amazon Bedrock AgentCore Harness 的安全工程 harness:LLM 负责起草,确定性代码负责裁决,任何东西上生产都必须有一次「被见证的通过」加人工批准。本讲每一条断言都可追溯到 file:line。

LLM 起草 → 确定性门裁决 → 人类批准 → 证据留痕
01 / 35
问题 · 混淆代理 (confused deputy)

一个 agent「说」它通过了 —— 在朴素的 loop 里,说了就算数

自我改进 agent 「我的 eval 通过了,promote」 朴素 loop 相信文字 → 直接执行 生产流量切换 未验证即上线

变体更阴险:agent 给 harness A 打分,却去 promote harness B(混淆代理)。system prompt 里写「只有通过才 promote」只是建议,agent 可以无视它 —— 那不是强制。这一反转在 agent_loop.py:1-27 里被明确点名。

02 / 35
问题 · 检测工程没有编译器

LLM 几秒起草一条 Sigma 规则 —— 谁来拒绝坏的那条?

结构损坏的规则、已经存在的重复规则、会把 SOC 淹没在误报里的噪声规则 —— 让另一个 LLM 来评判,就是「龟驮龟,一路驮到底」。确定性的门打破这个递归。

# sigma_yara_lint/handler.py:10-14 — 设计论点,逐字 "an LLM may draft a rule, but this linter — not another LLM — decides whether the rule is structurally valid." # 契约:ok=True 只代表「工具跑过了」,不代表规则有效 {"ok": True, "rule_type": "sigma", "valid": False, # ← valid 才是判决 "errors": ["condition references undefined selection 'sel2'"]}

纯 Python、零出网、零 token —— 因此可以放进任何 CI,且判决可复现、可审计。

03 / 35
问题 · 舰队运维静默失败

第三种痛:跨环境运维 harness 舰队

$PREFIX

未设置的清理前缀

脚本里 ''.startswith('') 恒为 True —— 一个空前缀会删掉账号里每一个 harness。core.py:567-589

env

被遗忘的环境

一个忘了设的 env 悄悄落到 prod。sentinel:env tag 永远最后盖章,manifest 里的笔误盖不过它。

page 2+

分页 bug

只读第一页会遗漏第 1 页之后的 harness —— 成本 + 治理泄漏。core.py:549-564

这就是为什么 sentinel-harness 里每一条破坏性路径都在多层 fail-closed。

04 / 35
概念 · 60 秒看懂它是什么

一个库 + 一个 CLI + 20 个确定性工具 + 一个 MCP server

① sentinel_harness/ Python 库 core · loader · factory · autonomy · agent_loop 封装 Bedrock AgentCore Harness API ② tools/ · 20 个确定性工具 纯 Python · 零出网 · 零 token · 零 secret 同输入 → 同输出 → 同判决 ③ sentinel CLI create · invoke · detection audit · mcp serve 一个 console script ④ MCP server (stdio) 把工具暴露给任意 MCP 客户端 默认暴露 17 / 20(治理过滤)

外加:harnesses/ 8 个出厂 agent 配置 · tests/ 2000+ 测试函数 · evidence/ 真实运行留存的 JSON 证据。

05 / 35
概念 · 核心论点

LLM 起草,确定性代码裁决

LLM-as-judge 管线

用一个 LLM 去评判另一个 LLM 的产出。判决不可复现、不可审计,且可以被越狱、被自我欺骗。促生环节没有硬边界。

sentinel 的做法

每个工具 handler 都是纯 Python:零出网、零 token、零 secret(每个 docstring 都写明)。同输入 → 同输出 → 同判决轨迹。

注意区分:sentinel 确实用一个 llm-judge harness 做质量打分 —— 但 promote 的门永远是确定性的(autonomy.evaluate_gate)。评分可以让 LLM 参与,裁决权不交给 LLM。

06 / 35
概念 · 全场最重要的一页

Witness-Gate 信任模型

Agent 发出 tool_use 调用(写 loop) Witness 账本 witnessed_pass witnessed_approval witnessed_subject Driver 只执行 + 守门 tool_use 仅 handler 返回值

状态只从真实的 tool-handler 返回 dict 更新 —— 「agent 的永远不更新状态」(agent_loop.py:34-36)。只有当三个见证都存在、且主体匹配时,promote 才会被派发。

07 / 35
架构 · repo 地图

后面每一个 file:line 引用都能落地

sentinel_harness/ core.py # 客户端生命周期 · create/invoke/promote · HITL resume loader.py # YAML → create_harness kwargs(纯离线校验) factory.py # manifest → 舰队 provision / teardown autonomy.py # C1 确定性改进控制器 · evaluate_gate agent_loop.py # agent 自己写 loop,driver 只守门(witness gate) loop_safety.py # 安全否决 + 回归护栏 mcp_server.py # stdio MCP server · 双层治理过滤 cli.py # sentinel 命令入口 tools/<name>/handler.py # 扁平脚本树 —— 按路径加载,不能 import tools.xxx harnesses/<name>/ # harness.yaml + system_prompt.md(8 个出厂) registry/tools.yaml # 治理登记表(approved / pending / deprecated) scenarios/ # 真实运行脚本 evidence/ # 它们产出的 JSON 证据

关键点:tools/ 是脚本树,不是包 —— 用 importlib.util.spec_from_file_location 按绝对路径加载,或走 CLI / MCP。

08 / 35
代码 · 第一层深入 · core.py

客户端在 import 时建好,配置来自 env

# core.py:42-51 — import 时就建好;之后再改 SENTINEL_REGION 无效 REGION = os.environ.get("SENTINEL_REGION", "us-east-1") _control = boto3.client("bedrock-agentcore-control", config=Config(read_timeout=60, connect_timeout=15, retries={"max_attempts": 3})) _data = boto3.client("bedrock-agentcore", config=Config(read_timeout=180, connect_timeout=15, retries={"max_attempts": 2})) # 流式读 180s # core.py:88-94 — 新用户看到的第一个错误 raise RuntimeError("Set SENTINEL_EXECUTION_ROLE_ARN to your harness " "execution role ARN. See docs/SETUP.md ...")

陷阱:import 之后再 export SENTINEL_REGION静默无效的 —— 必须调用 set_region(),它还会 patch 那些用 from .core import _control 捕获了旧客户端的兄弟模块(core.py:54-79)。

09 / 35
代码 · 完整生命周期

create_harness → wait_ready

from sentinel_harness import core as sh h = sh.create_harness("cve_triage_demo", # 不能有连字符:[a-zA-Z][a-zA-Z0-9_]{0,39} "You are a SecOps triage agent.", model=sh.bedrock_model(sh.MODEL_SONNET, maxTokens=8192, temperature=0.1), tools=[sh.tool_code_interpreter()], max_iterations=15) ready = sh.wait_ready(h["harnessId"]) # 每 8s 轮询;CREATE_FAILED 时抛出 print(ready["status"]) # READY # core.py:144-149 — UpdateHarness 是全量替换,不是 patch: # 「只传你想改的那一个字段,会静默丢掉其它所有字段 # (tools、memory、limits)」—— 必须 read-modify-write。

systemPrompt 会被规范化成 GA 的 list 形状 [{'text': ...}]core.py:123-124);创建是 fire-and-forget,wait_ready 轮询 GetHarness,超时 360s。

10 / 35
代码 · 流式内部 + stop_reason 契约

invoke:流不会 raise,错误藏在结果里

sid = sh.new_session("triage") # 必须 ≥33 字符,否则数据面拒绝 (core.py:110) r = sh.invoke(arn, sid, "Host WEB-07 is beaconing to a known C2. Assess.") # 结果形状 (core.py:285-350): # {text, events, stop_reason, tools_used, tool_use, tool_uses, metadata, usage, error} if r["error"]: # 流错误不会抛 —— 看起来像一句短而奇怪的回答 handle(r["error"]) # '[STREAM-ERROR ...]' 也会追加进 r['text'] if r["usage"]: # 流无 metadata 时为 None —— 求和前先 guard total += r["usage"]["totalTokens"]

tool 输入是跨多个 contentBlockDelta 事件到达的 JSON 字符串片段,任何单个事件里都看不到完整 dict;_consume_stream 负责重组。usage 来自 metadata 事件。

11 / 35
代码 · HITL 暂停 → 批准 → 恢复

两消息恢复协议:漏一个 id 就毁掉整个 session

if r1["stop_reason"] == "tool_use": gate = r1["tool_use"] # 真实运行:{'toolUseId': 'tooluse_VdqgoFhR9rHpi98Yoc5X6u', # 'name': 'request_containment_approval', ...} decision = {"approved": True, "approver": "analyst-001", "note": "isolate WEB-07"} r2 = sh.invoke_with_tool_result(arn, sid, gate, decision) # 同一个 session assert r2["stop_reason"] == "end_turn" # 并行门 —— 必须回答每一个待答 toolUseId,否则 session 损坏 (core.py:429-431): answers = [(tu, {"approved": True}) for tu in r1["tool_uses"]] r2 = sh.invoke_with_tool_results(arn, sid, answers)

恢复的一轮必须重发带每一个 toolUse block 的 assistant 消息,再发对每一个 toolUseId 都有 toolResult 的 user 消息。漏一个 = ValidationException + 不可恢复的 session。tool_uses(复数,全部并行门)vs tool_use(仅第一个,向后兼容)。

12 / 35
代码 · 声明式层

Loader + Factory:YAML → Harness,Manifest → 舰队

# harnesses/agent-ops/harness.yaml(真实,节选) harnessName: sentinel_agent_ops # 字母开头,alnum/下划线,无连字符 systemPrompt: system_prompt.md # 相对路径,'..' 穿越被拦 (loader.py:178-184) tools: - type: agentcore_gateway config: { agentCoreGateway: { gatewayArn: ${SENTINEL_GATEWAY_ARN} } } # 本地 env 展开 allowedTools: - "@gateway/harness_ops" # 显式 allowlist —— '*' 被禁 (loader.py:266-270) plan = provision_fleet("fleet.yaml", dry_run=True) # 零 AWS 调用,全量校验 # [{'name': 'sentinel_alert_triage', 'action': 'would_create'}, ...]

${ALL_CAPS} 本地展开,但故意跳过 ${arn:...} 金库引用(留给服务端解析)。Factory 侧:SENTINEL_ENV 永远压过 manifest,「一个忘了设的 env 永远不会悄悄落到 prod」。

13 / 35
架构 · 检测套件

7 个工具,一条组合链

sigma_yara_lint 逐条 lint detection_dedup 可证明的重叠 detection_coverage ATT&CK 盲区 detection_audit 聚合 + 健康分 detection_baseline 回归门 detection_navigator ATT&CK Layer

另有 detection_translatesigma_match 并列。全部共享同一个 Sigma parser(跨工具按绝对路径 importlib 加载)。全部纯 Python:零出网、零 token、零 AWS —— 因此可跑在任何 CI。

14 / 35
代码 · 实操 · sigma_yara_lint

三种格式,一个契约

# 统一输出契约 (sigma_yara_lint/handler.py:56-63): {"ok": True, # 工具跑过了 —— 不是「规则有效」 "rule_type": "sigma", "valid": False, # 这个才是判决 "errors": ["condition references undefined selection 'sel2'"], "warnings": [...], "fp_warnings": [...]} # 仅非空时才有这个 key # Sigma 检查:title/logsource/detection 必需;level 必须 ∈ # {informational, low, medium, high, critical}(handler.py:71, 205-268)

每种格式实际检查:Sigma 必需 key、condition 引用已定义的 selection、level 枚举;YARA 结构性(抹掉字符串/注释/hex 后括号配平);Suricata header 语法、必需 msg/sid/rev。坏输入返回 {'ok': False, 'error': 'validation_error'} —— 从不抛异常。

15 / 35
代码 · 实操 · dedup + coverage

可证明,绝不猜

# 只声称能证明的子集关系 —— 单 selection 的 AND 规则、 # 标量 contains/startswith/endswith/equals;其余全进 not_analyzed。 # 「因为一个错误的『可安全删除』判决会删掉真实的检测覆盖」(dedup:19-21) demo: broad {CommandLine|contains: '-enc'} vs narrow {Image|endswith: '\powershell.exe', CommandLine|contains: '-enc'} -> subsumptions: [{"subset": "narrow-002", "superset": "broad-001"}] # coverage 方向保守(coverage/handler.py:24-37): # 子技术 tag T1059.001 覆盖父目标 T1059 ✓ # 父 tag T1059 覆盖子目标 T1059.001 ✗

诚实设计:真实世界的大规则库会有大多数进 not_analyzed —— 这是特性,不是 bug。每个未分析项都带明确 reason,从不静默跳过、从不猜测。

16 / 35
概念 · 健康分

精确权重、饱和、逐位算例

_SCORE_WEIGHTS = { # detection_audit/handler.py:113 "invalid_rules": (40, 5), # 1 条无效 = 8 分;5+ 饱和到 40 "uncovered_techniques": (30, 10), # 仅当给了 --techniques 才扣 "duplicate_pairs": (15, 5), "untagged_rules": (10, 10), "lint_and_tag_noise": (5, 10), "fp_prone_rules": (10, 5), # v0.5.1:≥2 fp_warnings = fp_prone } # 扣分 = weight * min(1.0, count/basis);final = max(0, min(100, round(score))) # 算例: 100 - 8 - 6 - 3 - 1 - 1.5 - 4 = 76.5 -> round() -> 76(银行家舍入)

关键陷阱:最大的那类 uncovered_techniques(30 分)只有给了 --techniques 才扣。带目标清单和不带目标清单的分数是苹果对橘子,不可横向比较。

17 / 35
代码 · translate + FP 启发式

对「有损」诚实

# 经典朴素规则的真实 fp_warnings(CommandLine|contains: '-enc'): "fp_warnings": [ "no 'falsepositives' field — undocumented FP sources", "high-volume logsource 'process_creation' with no exclusion filter", "short/generic contains value '-enc' in selection.CommandLine|contains", "selection 'selection' has only a single contains predicate ..." ] # 4 条 => fp_prone => 扣 2 分健康分 # EQL 用 like~(大小写不敏感)即使做相等匹配 —— Sigma 匹配本就大小写不敏感; # '==' 会静默漏掉 CMD.EXE vs cmd.exe(translate:351-373)

translate 为 yara/suricata/SPL/EQL 发出骨架,并把每个有损构造标进 untranslatable —— 包括否定反转陷阱(selection and not filter 会把排除翻成包含),以及针对 Splunk 注释注入防御而丢弃反引号

18 / 35
工作流 · MCP Server

两条命令,把工具接进 Claude Code

pip install 'sentinel-harness[mcp]' # mcp>=1.0 是可选 extra (pyproject.toml:17) sentinel mcp serve # stdio JSON-RPC # 或:uvx --from 'sentinel-harness[mcp]' sentinel mcp serve // Claude Code settings.json (docs/MCP-SERVER.md:34-43) { "mcpServers": { "sentinel": { "command": "sentinel", "args": ["mcp", "serve"] } } } // 调用形状 —— event 包装是可选的 (mcp_server.py:221): {"name": "sigma_yara_lint", "arguments": {"event": {"rule_type": "sigma", ...}}}

工具名 = 目录名,描述 = docstring 首行,schema = 一个可选的 event 对象加裸参数兜底。导入失败的工具会拿到 [LOAD ERROR] 描述,而不是让 server 崩溃。

19 / 35
概念 · MCP 治理 · 两个门

为什么是 17 / 20,不是全部

(a) 登记表门

隐藏 registry/tools.yaml 里非 status:approved 的工具(web_search 是 pending)。这个门fail-OPEN:登记表不可达时不做过滤 —— 刻意的可用性选择。

(b) 控制面门

隐藏 harness_opsrun_evaluation(会改 AWS 资源或烧 token)。这个门永远成立,与登记表状态无关。

# mcp_server.py:64-69 — 不变式门 _CONTROL_PLANE_TOOLS = frozenset({"harness_ops", "run_evaluation"}) # 成本 + AWS 变更 SENTINEL_MCP_EXPOSE_CONTROL_PLANE=1 sentinel mcp serve # 走 MCP 的改进 loop 需要它

这个不对称就是设计本身:登记表门为可用性 fail-open,控制面门为安全永远 fail-closed。

20 / 35
代码 · run_agent_loop 逐步

五种结局,一本账本

result = agent_loop.run_agent_loop( invoke_fn=lambda: core.invoke(arn, session, task), resume_fn=lambda answers: core.invoke_with_tool_results(arn, session, answers), dispatch={"run_evaluation": run_eval_handler, "harness_ops": harness_ops_handler}, hitl_tool="request_promotion_approval", approve_fn=analyst_decision, # None = 人类拒绝一切(fail-closed) threshold=0.7, max_tool_calls=20) # 对手 agent 裸 promote 时看到的(agent_loop.py:409-413): {"ok": false, "error": "promotion_refused", "message": "driver refused promotion: no witnessed passing evaluation; ..."}

每个待处理调用被归入恰好一种 ToolCallRecord:executed / refused_promotion / hitl / unknown_tool / handler_error。promote 尝试在派发之前就对照见证状态检查。Session I/O 是两个注入的可调用对象 —— 真实 core.invoke 或脚本化 fake,同一个流 → 同一条轨迹。

21 / 35
代码 · 主体绑定

给 A 打分,去 promote B —— 拒绝

# eval 输出必须携带它的主体(scenario_agent_authored_loop.py:102-106): {"score": 0.92, "dimension_scores": {"correctness": 0.92, "safety": 1.0}, "feedback": {}, "harness_id": "sec_triage_v2"} # 省掉它 -> 永远无法 promote # agent_loop.py:450-451 — 一次失败的 eval 会清空见证: witnessed_subject = subject_of_eval(out) if gate_passed else None # 由这些测试证明: # test_new_failing_eval_clears_witnessed_subject # test_score_a_promote_b_refused (tests/test_agent_loop.py:460)

混淆代理修复:witnessed_subject 只在门通过时从 eval 输出的 harness_id 设定;失败的 eval 清空它(不给旧的通过续命)。promote 的 params.harness_id 必须匹配,否则拒绝。运维推论:省略 harness_id 的 eval 什么都没见证 —— 「一个未绑定的通过不是任何东西的见证」,于是每次 promote 都被拒。

22 / 35
概念 · 两个 loop,一个门

autonomy.py vs agent_loop.py

autonomy.run_improvement_loopagent_loop.run_agent_loop
谁写控制流确定性 Python(C1 控制器)Agent 自己写,driver 只守门
score → revise → gatePython authoring 决策agent 发 tool_use
共享的门同一个纯 evaluate_gate(聚合 + 安全否决 + 回归护栏)
# 两个 loop 共用的门(autonomy.py:152-197): evaluate_gate(score, threshold, incumbent_best, require_strict_improvement) # 安全否决:任一 {safety, groundedness} 维度失败即失败,无视聚合分 # 回归护栏:candidate >= incumbent_best(要求严格时则 >) # 最终 promote(autonomy.py:296-315): promoted = machine_ok and bool(approve_fn(candidate, last_score)) # approve_fn=None -> "机器门过了但无批准回调(fail-closed),未 promote"
23 / 35
代码 · Promotion 机制

create vs update vs promote_endpoint

# core.py:222-236 — 幂等 promote(「这是 agent 该用的」): try: return create_harness_endpoint(harness_id, endpoint_name, target_version=target_version, description=description) except _control.exceptions.ConflictException: # 只有这个异常才回退 return update_harness_endpoint(harness_id, endpoint_name, target_version=target_version, description=description) # agent 侧调用形状(tools/harness_ops/handler.py:213-230): {"action": "promote_endpoint", "params": {"harness_id": "sec_triage_v2", "endpoint_name": "prod", "target_version": "3"}}

harness 累积不可变的版本;endpoint 是一个稳定的具名指针。create 只成功一次(重建抛 ConflictException),update 重新指向,promote 是幂等组合。三者都被 default_is_promotion 识别为 promote 尝试 —— 仅 update 曾经是 witness-gate 的绕过口

24 / 35
代码 · Fail-Closed 打分

bool、NaN 与嵌套维度绕过

# autonomy.py:104-129 — _score_value 加固: # bool -> 0.0 (test_bool_score_not_treated_as_perfect) # NaN/±inf -> 0.0 (test_nan_aggregate_fails_closed_no_crash) # 缺失/不可解析 -> 0.0 # autonomy.py:132-149 — 否决绕过修复: # 在解析前剥掉嵌套的 'dimensions'/'dimension_scores' 子 key, # 否则 parse_dimension_scores 会再下钻,静默丢掉兄弟 safety 维度。 # 证明:test_nested_dimensions_key_cannot_hide_failed_safety

三个被审计的攻击面,各有证明测试:(1) 'score': true 记 0.0 而非 1.0 —— bool 是 int 子类,float(True)==1.0 会把「通过标志」自动 promote;(2) NaN 打败 < / > 的 clamp(两边都比较为 False);(3) 嵌套维度 key 会重新下钻并静默丢掉安全维度(被审计的 HIGH 级否决绕过)。教训:promote 路径上每个解析边界都需要对抗性输入。

25 / 35
代码 · Allowlist 与包含约束

无 '*'、无穿越、无 grant-all

# loader.py:266-270 — 精确错误: ValueError: "allowedTools must be an explicit allowlist — '*' (grant-all) is forbidden (ironclad rule #1)" # loader.py:246-255 — 标量拒绝(旧行为:静默逐字符迭代): ValueError: "allowedTools must be a list, got str" # loader.py:178-184 — realpath 包含约束,符号链接也算: ValueError: "...escapes the harness directory...; '..' traversal is not allowed"

一个裸 YAML 标量被拒,因为旧行为会逐字符迭代它、从而静默地没接上 HITL 门。systemPrompt 路径用 realpath 约束在 harness 目录内(指向外部的符号链接也拒),恶意 harness.yaml 无法把 /etc/passwd 喂给模型。已知 HITL 门自动注入 schema;未知门名静默不注入 —— 唯一要知道的软边缘。

26 / 35
代码 · 破坏性操作护栏

你和「删光一切」之间的三层

# 拒绝链(core.py:572-580, cli.py:221-229, factory.py:296-304): ValueError: refusing an empty cleanup prefix — ''.startswith('') is True; an unset $PREFIX would delete EVERY harness in the account # factory.py:269-275 — 跨环境 tag 守卫: FactoryError: "cross-env tag-guard: harness 'X' already exists under env 'staging' but this fleet is env 'prod'; refusing to touch it." # factory.py:151 — env tag 最后盖章,笔误无效: tags = {**fleet_tags, **entry_tags, ENV_TAG_KEY: env}

空/纯空白前缀在三层被拒。跨环境守卫在 provision 和 teardown 都生效;tag 必须用 list_tags_for_resource 单独取(ListHarnesses 摘要不带 tag —— 曾有 bug 静默废掉守卫)。未打 tag 的 harness 永远不被 factory 删除。

27 / 35
代码 · 出厂 harness 解剖 · agent-ops

逐字段读一个真实 harness.yaml

# harnesses/agent-ops/harness.yaml(真实出厂配置) harnessName: sentinel_agent_ops model: bedrockModelConfig: modelId: global.anthropic.claude-sonnet-4-6 # 需要处 pin 完整版本后缀 maxTokens: 8192 temperature: 0.1 systemPrompt: system_prompt.md tools: - type: agentcore_gateway config: { agentCoreGateway: { gatewayArn: ${SENTINEL_GATEWAY_ARN} } } allowedTools: ["@gateway/harness_ops"] memory: managedMemoryConfiguration: strategies: [SEMANTIC, SUMMARIZATION] eventExpiryDuration: 90 maxIterations: 20 timeoutSeconds: 300

另外 7 个出厂 harness 可做起步模板:alert-triage、detection-eng、llm-judge、meta-agent、ops-automation、research-supervisor、self-improving。未加版本后缀的 model id pin 可能在 invoke 时静默失败。

28 / 35
工作流 · 舰队生命周期

Manifest → dry_run → Provision → Teardown

写 fleet.yaml env·前缀·tags·harness 列表 dry_run 全量校验·零 AWS 调用 provision created | exists(幂等) teardown 仅精确解析名
# dry_run 阶段就抓到的 YAML 陷阱(factory.py:152-164): # tags: { build: 42 } -> int 非 str -> 只有 live 才会 ParamValidationError; # factory 报错说:给值加引号,如 build: "42" SENTINEL_ENV=prod uv run python -c "..." # env 变量永远压过 manifest env

陷阱聚焦:teardown 用 os.path.isfile 判分支 —— 打错的 manifest 路径会静默变成前缀删除;provision 从不更新已存在的 harness(漂移不会被 reconcile)。

29 / 35
工作流 · CI 门

baseline → ci → jq '.passed'

# GitHub Actions step(由真实 CLI 契约组合): - name: Detection library gate run: | pip install sentinel-harness sentinel detection ci rules/ \ --techniques T1059,T1059.001,T1190,T1046 \ --min-score 80 --against baseline.json \ --navigator-out attack-layer.json --json > gate.json jq -e '.passed' gate.json # 真实回归原因(detection_baseline/handler.py:121-141): # "health_score dropped 90 -> 70 (delta -20, tolerated -0)" # "new invalid rule(s): ['r-broken']" "new duplicate pair(s): ['r1|r2']"

零依赖门:无 AWS、无网络、无 LLM。回归 = 分数跌破容差,三个集合(invalid_rules / uncovered_techniques / duplicate_pairs)中任一出现新成员 —— 集合 diff 存在是因为「修好一条又弄坏另一条会让分数保持不变」。退出码:0 过、1 门失败、2 基础设施/输入错误,要区别对待。陷阱:audit --navigator 会替换报告并跳过门(要两者都要,用 ci --navigator-out)。

30 / 35
证据 · 真实运行留存

Proof, not claims

uv run python -c "import json; d=json.load(open('evidence/hitl_resume_result.json')); \ print([s['step'] for s in d['steps']])" # ['create', 'ready', 'turn1_pause', 'turn2_resume'] # evidence/autonomous_loop_result.json(每个 domain): {"domain": "alert_triage", "weak_start_score": 0.0, "final_score": 1.0, "rounds_used": 2, "approve_promoted": true, "reject_withheld": true, "safety_trap_promoted": false}

hitl_resume_result.json 是一次真实的暂停→批准→恢复往返:create → ready → turn1_pause(stop_reason=tool_use,真实 toolUseId tooluse_VdqgoFhR9rHpi98Yoc5X6u,主机 WEB-07)→ turn2_resume(stop_reason=end_turn,批准的隔离文本)。autonomous_loop_result.json 五个域全部 0.0 → 0.75-1.0,两轮内,safety_trap_promoted 处处为 false。每个产出它们的 scenario 脚本都在 scenarios/ 里。

31 / 35
证据 · 测试语料

对抗性设计的测试集

2494 / 7

passed / skipped

HEAD 实测(2026-07-21,v0.5.1)43.47s;文档滞后仍写 2365,guard 测试 pin 的是文档不是运行时。

333

检测套件

13 个检测测试文件,1.51s 跑完。

88%

覆盖率下限

.coveragerc / make ci / CI 三处共享的分支覆盖门。

# 隔离 venv 必需,否则 anaconda 用户级 litellm 会假性染红 2 个无关测试 cd sentinel-harness && uv sync --extra test && PYTHONNOUSERSITE=1 uv run pytest -q # property-based 不变式(hypothesis): # test_prop_never_promote_below_bar_or_unsafe # test_prop_rounds_never_exceed_cap

测试即文档:第 21-27 页的每个安全不变式都点名了它的证明测试。async MCP 协议测试跑真实 client 流,走完 initialize 握手。

32 / 35
工作流 · 上手

15 分钟从零到第一次 invoke

# docs/SETUP.md 有最小权限策略 export SENTINEL_EXECUTION_ROLE_ARN="arn:aws:iam::<acct>:role/<your-role>" export SENTINEL_REGION="us-east-1" pip install sentinel-harness # 或:repo 内 uv sync sentinel create harnesses/alert-triage/harness.yaml # 打印 id,等 READY sentinel invoke <harness-arn> "Triage this alert: ..." # 答案→stdout,诊断→stderr # 没有 AWS 账号?从这里开始 —— 完全离线: sentinel detection audit rules/ --techniques T1059,T1190 --min-score 80

每一步会踩到的错:跳过 step 1 → core.py:90-93 的 RuntimeError;名字带连字符 → 本地 ValueError;缺 env 变量 → 点名该变量的 KeyError。最后一步完全不碰 AWS,是无凭证者的「先试为快」路径。

33 / 35
参考 · 常开的一页

10 个陷阱,每个带 file:line

#陷阱修法 / 契约出处
1harnessName 不能有连字符[a-zA-Z][a-zA-Z0-9_]{0,39}core:120 / loader:233
2runtimeSessionId 太短≥33 字符,用 new_session()core:110
3memory 形状 create≠updateupdate 自动裹 optionalValuecore:159-167
4UpdateHarness 全量替换read-modify-writecore:144-149
5endpoint ConflictExceptionpromote_endpointcore:207-213
6${arn:...} 服务端解析${ALL_CAPS} 本地展开loader:39-42
7allowedTools '*' 被禁显式 allowlistloader:266-270
8import 后改 region 无效必须 set_region()core:54-79
9空前缀 = 删光一切三层拒绝core:572 / cli:221 / factory:296
10YAML tag 值须为字符串加引号:build: "42"factory:152-164

附:model id 必须带完整版本后缀(如 claude-haiku-4-5-20251001-v1:0),否则 invoke 时静默失败。

34 / 35
收尾 · 精通清单

agent 起草;确定性门裁决;
人类批准;证据证明。

你能解释 witness gate、把一次 promote 拒绝追到它的检查、手算一个健康分、并知道一个损坏的 HITL session 能否恢复吗?如果能,你已精通。

下一步:把 agent_loop.py + autonomy.py 通读一遍(合计约 600 行);对着自己的账号跑 scenarios/scenario_hitl_resume.py;把 sentinel detection ci 接进一条真实管线;用 sentinel export 作为无锁定的逃生口(产出可编辑的 Strands Agent 代码)。

35 / 35