一个面向 Amazon Bedrock AgentCore Harness 的安全工程 harness:LLM 负责起草,确定性代码负责裁决,任何东西上生产都必须有一次「被见证的通过」加人工批准。本讲每一条断言都可追溯到 file:line。
变体更阴险:agent 给 harness A 打分,却去 promote harness B(混淆代理)。system prompt 里写「只有通过才 promote」只是建议,agent 可以无视它 —— 那不是强制。这一反转在 agent_loop.py:1-27 里被明确点名。
结构损坏的规则、已经存在的重复规则、会把 SOC 淹没在误报里的噪声规则 —— 让另一个 LLM 来评判,就是「龟驮龟,一路驮到底」。确定性的门打破这个递归。
纯 Python、零出网、零 token —— 因此可以放进任何 CI,且判决可复现、可审计。
脚本里 ''.startswith('') 恒为 True —— 一个空前缀会删掉账号里每一个 harness。core.py:567-589
一个忘了设的 env 悄悄落到 prod。sentinel:env tag 永远最后盖章,manifest 里的笔误盖不过它。
只读第一页会遗漏第 1 页之后的 harness —— 成本 + 治理泄漏。core.py:549-564
这就是为什么 sentinel-harness 里每一条破坏性路径都在多层 fail-closed。
外加:harnesses/ 8 个出厂 agent 配置 · tests/ 2000+ 测试函数 · evidence/ 真实运行留存的 JSON 证据。
用一个 LLM 去评判另一个 LLM 的产出。判决不可复现、不可审计,且可以被越狱、被自我欺骗。促生环节没有硬边界。
每个工具 handler 都是纯 Python:零出网、零 token、零 secret(每个 docstring 都写明)。同输入 → 同输出 → 同判决轨迹。
注意区分:sentinel 确实用一个 llm-judge harness 做质量打分 —— 但 promote 的门永远是确定性的(autonomy.evaluate_gate)。评分可以让 LLM 参与,裁决权不交给 LLM。
状态只从真实的 tool-handler 返回 dict 更新 —— 「agent 的话永远不更新状态」(agent_loop.py:34-36)。只有当三个见证都存在、且主体匹配时,promote 才会被派发。
关键点:tools/ 是脚本树,不是包 —— 用 importlib.util.spec_from_file_location 按绝对路径加载,或走 CLI / MCP。
陷阱:import 之后再 export SENTINEL_REGION 是静默无效的 —— 必须调用 set_region(),它还会 patch 那些用 from .core import _control 捕获了旧客户端的兄弟模块(core.py:54-79)。
systemPrompt 会被规范化成 GA 的 list 形状 [{'text': ...}](core.py:123-124);创建是 fire-and-forget,wait_ready 轮询 GetHarness,超时 360s。
tool 输入是跨多个 contentBlockDelta 事件到达的 JSON 字符串片段,任何单个事件里都看不到完整 dict;_consume_stream 负责重组。usage 来自 metadata 事件。
恢复的一轮必须重发带每一个 toolUse block 的 assistant 消息,再发对每一个 toolUseId 都有 toolResult 的 user 消息。漏一个 = ValidationException + 不可恢复的 session。tool_uses(复数,全部并行门)vs tool_use(仅第一个,向后兼容)。
${ALL_CAPS} 本地展开,但故意跳过 ${arn:...} 金库引用(留给服务端解析)。Factory 侧:SENTINEL_ENV 永远压过 manifest,「一个忘了设的 env 永远不会悄悄落到 prod」。
另有 detection_translate 和 sigma_match 并列。全部共享同一个 Sigma parser(跨工具按绝对路径 importlib 加载)。全部纯 Python:零出网、零 token、零 AWS —— 因此可跑在任何 CI。
每种格式实际检查:Sigma 必需 key、condition 引用已定义的 selection、level 枚举;YARA 结构性(抹掉字符串/注释/hex 后括号配平);Suricata header 语法、必需 msg/sid/rev。坏输入返回 {'ok': False, 'error': 'validation_error'} —— 从不抛异常。
诚实设计:真实世界的大规则库会有大多数进 not_analyzed —— 这是特性,不是 bug。每个未分析项都带明确 reason,从不静默跳过、从不猜测。
关键陷阱:最大的那类 uncovered_techniques(30 分)只有给了 --techniques 才扣。带目标清单和不带目标清单的分数是苹果对橘子,不可横向比较。
translate 为 yara/suricata/SPL/EQL 发出骨架,并把每个有损构造标进 untranslatable —— 包括否定反转陷阱(selection and not filter 会把排除翻成包含),以及针对 Splunk 注释注入防御而丢弃反引号。
工具名 = 目录名,描述 = docstring 首行,schema = 一个可选的 event 对象加裸参数兜底。导入失败的工具会拿到 [LOAD ERROR] 描述,而不是让 server 崩溃。
隐藏 registry/tools.yaml 里非 status:approved 的工具(web_search 是 pending)。这个门fail-OPEN:登记表不可达时不做过滤 —— 刻意的可用性选择。
隐藏 harness_ops 和 run_evaluation(会改 AWS 资源或烧 token)。这个门永远成立,与登记表状态无关。
这个不对称就是设计本身:登记表门为可用性 fail-open,控制面门为安全永远 fail-closed。
每个待处理调用被归入恰好一种 ToolCallRecord:executed / refused_promotion / hitl / unknown_tool / handler_error。promote 尝试在派发之前就对照见证状态检查。Session I/O 是两个注入的可调用对象 —— 真实 core.invoke 或脚本化 fake,同一个流 → 同一条轨迹。
混淆代理修复:witnessed_subject 只在门通过时从 eval 输出的 harness_id 设定;失败的 eval 清空它(不给旧的通过续命)。promote 的 params.harness_id 必须匹配,否则拒绝。运维推论:省略 harness_id 的 eval 什么都没见证 —— 「一个未绑定的通过不是任何东西的见证」,于是每次 promote 都被拒。
| autonomy.run_improvement_loop | agent_loop.run_agent_loop | |
|---|---|---|
| 谁写控制流 | 确定性 Python(C1 控制器) | Agent 自己写,driver 只守门 |
| score → revise → gate | Python authoring 决策 | agent 发 tool_use |
| 共享的门 | 同一个纯 evaluate_gate(聚合 + 安全否决 + 回归护栏) | |
harness 累积不可变的版本;endpoint 是一个稳定的具名指针。create 只成功一次(重建抛 ConflictException),update 重新指向,promote 是幂等组合。三者都被 default_is_promotion 识别为 promote 尝试 —— 仅 update 曾经是 witness-gate 的绕过口。
三个被审计的攻击面,各有证明测试:(1) 'score': true 记 0.0 而非 1.0 —— bool 是 int 子类,float(True)==1.0 会把「通过标志」自动 promote;(2) NaN 打败 < / > 的 clamp(两边都比较为 False);(3) 嵌套维度 key 会重新下钻并静默丢掉安全维度(被审计的 HIGH 级否决绕过)。教训:promote 路径上每个解析边界都需要对抗性输入。
一个裸 YAML 标量被拒,因为旧行为会逐字符迭代它、从而静默地没接上 HITL 门。systemPrompt 路径用 realpath 约束在 harness 目录内(指向外部的符号链接也拒),恶意 harness.yaml 无法把 /etc/passwd 喂给模型。已知 HITL 门自动注入 schema;未知门名静默不注入 —— 唯一要知道的软边缘。
空/纯空白前缀在三层被拒。跨环境守卫在 provision 和 teardown 都生效;tag 必须用 list_tags_for_resource 单独取(ListHarnesses 摘要不带 tag —— 曾有 bug 静默废掉守卫)。未打 tag 的 harness 永远不被 factory 删除。
另外 7 个出厂 harness 可做起步模板:alert-triage、detection-eng、llm-judge、meta-agent、ops-automation、research-supervisor、self-improving。未加版本后缀的 model id pin 可能在 invoke 时静默失败。
陷阱聚焦:teardown 用 os.path.isfile 判分支 —— 打错的 manifest 路径会静默变成前缀删除;provision 从不更新已存在的 harness(漂移不会被 reconcile)。
零依赖门:无 AWS、无网络、无 LLM。回归 = 分数跌破容差,或三个集合(invalid_rules / uncovered_techniques / duplicate_pairs)中任一出现新成员 —— 集合 diff 存在是因为「修好一条又弄坏另一条会让分数保持不变」。退出码:0 过、1 门失败、2 基础设施/输入错误,要区别对待。陷阱:audit --navigator 会替换报告并跳过门(要两者都要,用 ci --navigator-out)。
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/ 里。
HEAD 实测(2026-07-21,v0.5.1)43.47s;文档滞后仍写 2365,guard 测试 pin 的是文档不是运行时。
13 个检测测试文件,1.51s 跑完。
.coveragerc / make ci / CI 三处共享的分支覆盖门。
测试即文档:第 21-27 页的每个安全不变式都点名了它的证明测试。async MCP 协议测试跑真实 client 流,走完 initialize 握手。
每一步会踩到的错:跳过 step 1 → core.py:90-93 的 RuntimeError;名字带连字符 → 本地 ValueError;缺 env 变量 → 点名该变量的 KeyError。最后一步完全不碰 AWS,是无凭证者的「先试为快」路径。
| # | 陷阱 | 修法 / 契约 | 出处 |
|---|---|---|---|
| 1 | harnessName 不能有连字符 | [a-zA-Z][a-zA-Z0-9_]{0,39} | core:120 / loader:233 |
| 2 | runtimeSessionId 太短 | ≥33 字符,用 new_session() | core:110 |
| 3 | memory 形状 create≠update | update 自动裹 optionalValue | core:159-167 |
| 4 | UpdateHarness 全量替换 | read-modify-write | core:144-149 |
| 5 | endpoint ConflictException | 用 promote_endpoint | core:207-213 |
| 6 | ${arn:...} 服务端解析 | 仅 ${ALL_CAPS} 本地展开 | loader:39-42 |
| 7 | allowedTools '*' 被禁 | 显式 allowlist | loader:266-270 |
| 8 | import 后改 region 无效 | 必须 set_region() | core:54-79 |
| 9 | 空前缀 = 删光一切 | 三层拒绝 | core:572 / cli:221 / factory:296 |
| 10 | YAML tag 值须为字符串 | 加引号:build: "42" | factory:152-164 |
附:model id 必须带完整版本后缀(如 claude-haiku-4-5-20251001-v1:0),否则 invoke 时静默失败。
agent 起草;确定性门裁决;
人类批准;证据证明。
你能解释 witness gate、把一次 promote 拒绝追到它的检查、手算一个健康分、并知道一个损坏的 HITL session 能否恢复吗?如果能,你已精通。
下一步:把 agent_loop.py + autonomy.py 通读一遍(合计约 600 行);对着自己的账号跑 scenarios/scenario_hitl_resume.py;把 sentinel detection ci 接进一条真实管线;用 sentinel export 作为无锁定的逃生口(产出可编辑的 Strands Agent 代码)。