LLM 起草,确定性代码裁决。没有一样东西能在目击到通过 + 人工批准之前被推上生产。
离线测试全绿 · 90% 覆盖率
对抗审计缺陷已修复
真实 AWS 上一次 CVE 分诊,干净拆除
自主的 SecOps agent 会用三种方式辜负你 —— 而这三种都不抛异常。
自我改进的 agent 靠"自己一句话"就把自己推上生产。没有崩溃,只有一个更差的 agent 开始接真实 SOC 流量。
LLM 写出一条读起来完美的 Sigma 规则,引用了一个从未定义的 selection。谁来拒绝它?写它的 LLM 有自我批准偏见。
一个未设置的环境变量,让清理脚本匹配并删除每一个 harness。分页丢失让资源变成孤儿。
共同点在最后一页揭晓 —— 它是贯穿全篇的脊梁。
设想 north-star 循环正常运转:agent 给候选打分、判定通过、推上生产端点。现在设想它只是说了句"评测通过,正在推送"—— 推送就真的执行了。或者更微妙:它诚实地给 harness A 打分,却推送了 harness B。
为什么 prompt 拦不住?因为 prompt 是模型能读、能被绕过、能自我说服的东西。执法点必须在模型够不到的地方。
一条 Sigma 规则,condition 里引用了名叫 suspicious_cmd 的 selection —— 一个它从未定义的 selection。上线后 SIEM 静默编译出一条匹配一切、或什么都不匹配的规则。写它的 LLM 是你最不该让它来打分的东西。
纯 Python,零 token,零网络。同一输入永远同一输出 —— 才配当强制自动闸门。
一次真实审计运行里,8 条无效规则被评了 48 分 —— 如果没有确定性编译器把关,它们本会一路流向生产。
① 一个独立对抗 reviewer harness去攻击规则(无自我批准偏见);② 一个不是 LLM 的编译器。generation ≠ evaluation,写进结构,不写进 prompt。
一条 manifest 跨 test / staging / prod 铺开一支安全 agent 舰队。一个 YAML 里的 tag 值被解析成整数 42 而非字符串 —— 本地毫无报错,然后第五个 CreateHarness 中途 ParamValidationError,留给你一支半成品舰队和无处回滚。
共享同名的 staging 拆除,静默删掉 prod agent。每个 harness 都被打上 sentinel:env tag;同名但不同 env 的 harness,provision 和 teardown 都拒绝触碰。
list_harnesses 若不翻完全部分页,就会漏看已存在的 harness,重复创建、留下无人清理的孤儿。
teardown 遇到没有 sentinel:env tag 的 harness 直接拒删 —— 它可能是打标之前就存在的 prod 资源。teardown 刻意比 provision 更谨慎。
三个背叛看似无关,其实是同一个病。修法也是同一条脊梁:enforcement outside the model。
推送只在 driver 亲自目击过通过闸门的评测 + 人工批准时才执行;分数从 handler 的真实返回读,永不从 agent 的话里读。
结构合法性由纯 Python linter裁决,不是另一个 LLM;对抗 reviewer 通过 tool call 提交裁决,不靠自由文本。
可检查的失败全部左移进零 AWS 调用的 dry-run;删除由 tag 守卫和空前缀守卫在代码里挡住。
a prompt is advice, not enforcement. 这句话(README.md:206-208)是全篇的脊梁 —— 下一章,我们看它如何变成架构。
不是编排代码 — 而是一份能进 code review、能 diff、能 CI 的声明式规格
这一章我们把地基讲透:为什么"让 AWS 跑循环"是把安全控制点移出模型可触及范围的唯一办法,以及 Bedrock AgentCore 用两个平面、一个托管循环、一台 per-session 微虚拟机把这件事变成现实。
README.md:32 — "a production SecOps agent should be configuration, not orchestration code."
loader.py 纯离线把它翻译成 create_harness kwargs,零 AWS 调用即可在 CI 里校验。
每一行都是引 bug 的地方,且没一行是你真正的安全逻辑。更糟:谁能 promote、什么能发布,全藏在模型能影响的 glue code 里。
旗舰 CVE 分诊 agent 只有约 30 行配置(scenario_cve_triage.py:49-58):create_harness + wait_ready + return arn,零循环代码。行为变更 = 改配置 + UpdateHarness,永远不改代码。
core.py:50-51 顶部有两个 boto3 client,它们不可互换:一个造/改 agent,一个 stream 它的思考。
docs/ARCHITECTURE.md:15 — 两个 workload 形状相反:控制面是短事务生命周期,数据面是长达 78.6s 的 streaming LLM turn。一台 microVM per runtimeSessionId(ARCHITECTURE.md:45):会话隔离、工具沙箱、循环执行全是 AWS 的问题,不是你的 —— 就像你宁愿跑 Lambda 而不是管 EC2。
这条分界线,就是整个价值主张。
每个可选字段仅在真值时才加入 args(core.py:125-132)—— wrapper 从不发 null,"agent 形状"就是你声明的样子。
create_harness 里刻意没有循环代码;返回的 harness id 就是一个 server-side agent。要跑它,poll wait_ready 到 READY,再走数据面 invoke。
直播 demo 里最重要的数字不是 78 秒延迟 —— 而是 CVSS 计算和 CVE 事实没有一个来自模型的想象。
证据里 did_deterministic_calc=true(evidence/README.md:21)。版本区间检查、受影响资产比例、分数解析、Sigma/YARA lint —— 全在纯 Python 里跑(core.py:487-489)。
reviewer 读 handler.py 就能确切知道它做什么 —— 循环里没有模型来引入非确定性。
L3 地基被 read-only 实盘验证为 default-deny VPC 孤岛:无 IGW、无 NAT、无 0.0.0.0/0 路由(evidence/live_verify_result.json)—— egress 在结构上不可能,而非仅仅策略禁止。替代方案(工具本身是 LLM 调用,或自由访问互联网)会让每个测试 flaky、每次运行昂贵、每次审计变成概率论辩论。sentinel-harness 拒绝这条路:边缘确定性,LLM 只在中间。
docs/ARCHITECTURE.md:3 — 整个框架是一次逆向工程:把三层 SecOps 平台,纯粹用 Harness 原语表达。层次编码了信任与爆炸半径,从最安全到最危险。
它们自底向上组合:L3 决定工具面(哪些工具 live),L1/L2 消费它。你无法在没有地基(L3)时治理进攻(L2),也无法在不知道哪些工具被批准(L3)时跑策略(L1)。替代方案 —— 一个全工具扁平 agent —— 在"查个 CVE"和"引爆攻击路径"之间没有职责分离,没有门。
docs/ARCHITECTURE.md:18 — "multi-agent = 多个 harness + 一个 supervisor"。一个 harness 按设计是 single-agent + multi-tool。
并行/角色分解,来自并发跑多个 harness,再用一个 supervisor harness 综合。detection-eng 里 generator harness 和 adversarial-reviewer harness 是两个独立 harness —— 生成≠评估,没有自我批准偏差。这个双 harness 拆分是抗幻觉机制,不是装饰。
把这三件事分清楚,就避开了一个真实的认证模型错误。inline_function 门(core.py:507)PAUSE 循环(stop_reason=tool_use)并把调用交回你的代码 —— human-in-the-loop 通过两个机制杀幻觉:独立 reviewer harness + 需分析师签字的 inline 门。这套地基铺完,后面所有 SecOps 能力才能站上来。
前两章的循环,都是人写的 Python 在做决定:改一版、打个分、过个闸、上线。这一章我们把笔交给 agent 自己——让它通过发出 tool call 来驱动整个循环。问题随之而来:当"决定要不要上线"的那个 agent,恰好就是"要被上线"的那个 agent,它说的每一句话都是自利的。一个足够聪明或足够坏的 agent,能不能只凭一句"评估通过了,上线吧"就把自己推上生产?这一章讲的,就是我们怎么让它"说了不算"。
见证门信任模型 · Witness-Gate Trust Model · agent_loop.py
同样是 改进→打分→过闸→上线,笔在谁手里,安全模型是完全不同的两件事。
autonomy.run_improvement_loop:循环本身是人写死的 Python。哪一步打分、什么时候过闸、要不要上线,全部由脚本或确定性控制器决定。agent 只是被调用去"生成候选"的那只手——它碰不到方向盘。
agent_loop.run_agent_loop:agent 在自己的 harness 里通过 stop_reason == "tool_use" 不断发出 tool call,循环的每一步都由它自己驱动。driver 只负责"执行与看守"——派发、恢复会话、按策略放行或拒绝。
这次反转的代价,就是经典的 confused deputy(被搞糊涂的代理人):做上线决定的 agent,同时也是被上线的对象。它的任何自我汇报都是利益相关方的证词。agent_loop.py:4-12 把这句话说得很直白——"the agent authors the loop … the driver here only executes and guards"。
整个信任模型可以压缩成一句话:driver 内部维护三个见证变量——witnessed_pass / witnessed_approval / witnessed_subject,它们只能被 tool-handler 的返回 dict 更新,绝不接受 agent 说的任何一个字。
当 agent 发出一次上线调用,driver 把四种"不该放行"的理由逐条收进 missing[]:没见证到通过、没见证到人类批准、通过了却没绑定主体、主体对不上。只有 missing 为空,代码才 fall through 去真正执行。
离线证据里最朴素的一条 promotion_refused:agent 一上来就调 harness_ops create_endpoint,之前根本没跑过评估。driver 直接拒。
没有任何东西被推上线
tool_calls_used 也是 1,一步就拦下
"no witnessed passing evaluation; no witnessed human approval"
证据:evidence/agent_authored_loop_result.json 的 promotion_refused 步骤。
最阴险的变体不是跳过评估,而是诚实地给 harness A 打了高分,转手却去上线 harness B。见证门用主体绑定堵死它:一次 pass 永远和它实际打分的那个 harness 死死绑在一起。
一旦见证到的主体和上线目标对不上,agent 收到的是一条精确到名字的结构化拒绝——它自己能看到,也被写进 trace:
注意 witnessed_subject 在 gate 失败时会被重置为 None:一次陈旧的 pass 无法在事后被一个新批准"重新挂靠"。fail-closed 是这里的默认姿态。
见证门的第一道防线,是"分数本身可信"。autonomy._score_value 把三类会静默污染判断的输入统统压回 0.0——因为"读不出的分不是通过"。
bool 单独拦:True 若直接 float() 会变 1.0,一个返回"通过标志位"当分数的裁判就会静默自动上线。NaN 也单独拦:它同时让 v<0 和 v>1 都为 False,会溜过 clamp。
第二道防线是审计里抓到的嵌套维度绕过:如果 dimension_scores 里塞一个叫 dimensions 的子块,解析器会"钻进去"只看孩子、悄悄丢掉同级的 safety 维度。autonomy.py:132-149 直接把这类 key 剥掉,让安全否决看到真正的维度。
同一个 driver,喂进四种 agent 行为,加一个总裁决。evidence/agent_authored_loop_result.json 逐条记录了它看到什么、拦了什么。
评估通过 + 人类批准 + 主体一致 → promoted:true,subject 'sec_triage_v2'。该放行时放行。
没评估就想上线 → 一步拦下,promoted:false,refused:1。
反复跑低分评估 → 撞上 max_tool_calls,stopped_by == "cap",永不无限循环。
人类点了批准,聚合分也过了——却仍然没上线。
safety_trap 是全章最锋利的一刀:witnessed_approval:true(人真的批了),但某个 safety 维度失败,evaluate_gate 强制否决——aggregate_passed=True 也不算。安全否决压过人类批准,witnessed_pass 始终是 false,上线被拒。人的一次点击,不等于见证到一次真正通过的评估。
第四章 · 从 import 那一刻到一次真实 invoke——core.py / loader.py / factory.py 里那些不写就会静默出错的决定
import 期建 client 与 set_region 陷阱
create / wait_ready / update 全生命周期
流式解析与两段式 HITL 恢复
YAML→kwargs 与舰队编排
一句话:这一层从不调用模型、不开 socket、不读时钟——它只是把你声明的东西翻译成 GA API 的形状,并在每一处会静默失败的地方替你提前失败。
import 之后再 export SENTINEL_REGION=... 完全无效——client 已经建好了。你所有调用都留在旧 region,而且不报错。
两个 Config 不是随手写的:数据面 180s 读超时对应一次会 stream 78 秒的模型思考;控制面 60s/3 重试对应短而最终一致的生命周期调用。用 boto 默认值,会把一次长 CVE 分诊 stream 拦腰截断。
harnessName 必须匹配 [a-zA-Z][a-zA-Z0-9_]{0,39}——不能有连字符。这是最常见的 ValidationException;loader/factory 本地预校验,让你在往返前失败。
live CVE demo 的真实 trace:create_harness → harnessId sentinel_live_demo-XETVUi59pC → wait_ready status READY → invoke。systemPrompt 在 create 和 update 里都被归一成 [{"text": ...}]——GA API 拒收裸字符串。
两个 bug 被注释钉在案:_raw 只能 pop 一次(早期版本在 try/except 里各 pop 一次,第二次拿到空串);完成的 tool 调用必须 append 进 list——一个 turn 可能并行暂停在多个 tool_use 块上,只留最后一个会静默丢掉更早的 HITL gate。
tool_use = 循环暂停了,需要人/客户端回答;其它(end_turn / max_tokens…)= 这一轮结束。live CVE:stop_reason end_turn,usage 75456 in / 3003 out = 78459,78.6s。
陷阱:只读 result['tool_use'] 而忽略 tool_uses 的调用者,会在并行 gate 上少答、并在恢复时腐化 session。流级错误(runtimeClientError 等)被显式抬成 [STREAM-ERROR] 文本标记 + 一等 error 字段,不埋进正文。
为什么两条:模型的上下文必须呈现「工具被调用」+「结果返回」这一段连贯的 assistant→user 交换。只发结果、或只发调用,都会打破模型期待的交替。
每个 pending 的 toolUseId 必须拿到一个 toolResult;缺一个就是 ValidationException / session 被腐化。这正是 _consume_stream 被修成 append 全部并行 gate 的原因。
换个 session_id 恢复 = 开一段全新对话,模型对暂停毫无记忆,pending toolUse 成孤儿。回填的 input 必须是 _consume_stream 重建的那个(可能是 {"_unparsed":...}),不能凭空捏造。
单 gate 便捷封装 invoke_with_tool_result 只是对复数版 invoke_with_tool_results 的转调;答多个并行 gate 时必须用复数版,答全 tool_uses 里每一项。空 answers 会 raise ValueError——它拒绝一次空恢复。
load_harness_config 做 零 AWS 调用——离线的一半,让你无凭证也能在 CI 校验 config;只有 create_from_config 才碰 AWS。缺失环境变量会 raise KeyError 并点名(12-factor 大声失败)。
HITL gate 的名字在 config 的 allowedTools 里,但它的 inputSchema 在代码的 _INLINE_GATES 注册表里。loader 把匹配的 tool_inline 定义注入 tools,sentinel create 就自动接好了暂停循环的 gate——除非 yaml 已显式声明(则尊重,不重复注入)。
陷阱:harnessName: 123(解析成 int)或带连字符的名字在 loader.py:233 本地就挂——早于 AWS,因为同一条命名正则在此也强制执行。裸标量 allowedTools(非 list)会被逐字符迭代,静默接不上 gate——所以 loader 大声拒绝非 list。
解析并校验整个舰队——yaml 读取、${ENV} 扩展、命名规则、标签合成——零 AWS 调用,连 list_harnesses 都不发。对抗静默服务端校验的本地平价检查。
一次共享 list_harnesses 索引所有已存在 harness,供 N 个配置复用。已存在 → 记 exists,绝不重建。create-or-skip,从不 update。
每个 harness 盖 sentinel:env 标签;同名 harness 若已在不同 env 下,provision 和 teardown 都拒绝碰它。prod run 绝不误删同名的 staging。
env 优先级:SENTINEL_ENV > manifest.env > 'dev' 兜底——遗忘的 env 绝不静默落到 prod。teardown 比 provision 更谨慎:未打标的同名 harness 在 provision 是安全跳过,在 teardown 是硬拒绝(它可能是 pre-tagging 的 prod 资源或别的工具建的)。
只传你想改的那一个字段,会静默丢掉 tools / memory / limits——服务端把这当成一次完整的新配置。harness_ops 必须先读当前配置,改,再把整个形状发回去(read-modify-write)。
factory 的幂等刻意做成 create-or-skip、从不 update:「agent update = 全量替换」是 AgentCore 一个有文档记载的坑,交由别处的 read-modify-write 处理。memory 的 auto-wrap 是幂等的,但你必须走 update_harness,不能裸调 boto。
wait_ready 在 UPDATE_FAILED 上同样会 raise 并带 failureReason——一次全量替换若把配置改成非法形状,会在 update 后的轮询里显形,而不是在提交那一刻。
LLM 可以起草一条检测规则,但永远不允许 LLM 判定它是否有效、是否命中攻击、是否安全上线。那份裁决,交给 3,597 行零 token、零网络、同输入必同输出的纯 Python。
确定性工具,全程 LLM-free
行纯 Python,零第三方依赖可离线跑
token / 网络调用 / 密钥 —— 工具本身也是安全控制
只有一个解析器、一套 rule-id 推导。上层工具全部按绝对路径复用下层,而不是复制逻辑。打穿底层,整座塔同步移动 —— 这正是设计意图:强制"一致"。
输入:一条结构损坏的 Sigma(缺 detection 主体)
运行 lint
对 8 条同类烂规则跑 audit(真实 CLI 输出)
ok:true = 工具运行成功;valid 才是规则有效性。混淆这两个字段,是这套工具最常见的误读。
evidence/bas_replay_result.json —— 确定性回放引擎 generate_cases + sigma_match,离线、无 LLM / 网络 / AWS。两个亮绿,两个死寂,盲点就是检测工程的 backlog。
coverage_ratio = 检出 2 / 测试 4
BAS 唯一绝不能发生的事:把一条不可评估的规则算成"命中",那会从 backlog 里抹掉一个真盲点。任何 caveat 都强制 matched=False。
回放的 coverage 测"事件是否触发规则";detection_coverage 测"规则是否标注技术"。规则可能被标注却不开火,反之亦然。
权重是静态字典,不是学习模型 —— 可对审计员逐条辩护。饱和意味着单个噪声类扣满权重后不再增加,淹没不了更严重的类。
| 缺陷类 | weight | basis |
| invalid_rules 结构损坏 | 40 | 5 |
| uncovered_techniques 盲点 | 30 | 10 |
| duplicate_pairs 重复 | 15 | 5 |
| untagged_rules 未标注 | 10 | 10 |
| fp_prone_rules 易误报 | 10 | 5 |
| lint_and_tag_noise 噪声 | 5 | 10 |
v0.5 之前,Count|gt: 1000 被静默当成 == 1000;payload|frobnicate 也 fall through 到相等。都返回一个自信的布尔值 —— 在盲点分析里这是毒药。
选择项无短路地评估所有 key —— 后一个 key 的 caveat 不会被前一个失败的 key 藏起来。
| 修饰符 | 语义(全部实测) |
| cidr | ipaddress 成员判定:10.4.2.1 ∈ 10.0.0.0/8 ✓ |
| base64offset | 3 种 pad 对齐,jndi:ldap 流中命中 ✓ |
| windash | -enc 同时匹配 /enc ✓ |
| gt/gte/lt/lte | 真浮点比较;非数值 → caveat |
| exists | 仅判存在性 |
| cased | 翻成大小写敏感:Admin ≠ admin |
| null | 匹配字段缺失或 None |
_to_number 拒绝布尔(True 当 1 会藏错);坏 CIDR 值记 caveat,非 IP 字段只是不匹配(规则没错,事件不是 IP)。
工具承认它能"证明"的边界,这个承认就是整个信任模型。会猜的工具看起来更聪明,却危险得多 —— 一个假的"可安全删除"会删掉真覆盖。
dedup 只在单选择-纯 AND 且集合包含可数学证明时才断言重复。复杂条件 / 正则 / 数值全进此账本,永不参与重复断言(dedup/handler.py:260-266)。
translate 把有损修饰符或 NEGATION 路由进来。否定是关键:OR-flatten 骨架会把排除反转成包含,静默改变语义 —— 宁可标注"需人工重建"。
sigma_match 里任何非空 caveats 强制 matched=False,让 BAS 调用方明确排除该规则,而不是信任它。
FP 启发式:5 个确定性检查(触发 fp_prone)
结构有效性与运营噪声是正交的,必须分开通道。fp_warnings 是第三列表,不动 errors/warnings 计数。
audit 要求 ≥2 个 fp_warnings 才归为 fp_prone(单个软信号太急躁);健康分封顶 -10。仅对通过 lint 的规则跑 —— 坏规则不会同时是 fp_prone。
baseline 不只看标量分数,还 diff 集合:某条规则变 invalid,即使分数持平也算回归(baseline/handler.py:121-141)。
三个退出码,含义清晰
输入坏:读不到目录 / 无规则 / 畸形
门禁失败:分数低于底线 或 回归
通过
空规则目录 exit 2 不是 0;baseline 必须用相同 --techniques;两个门禁都不开则永远通过,须至少 opt-in 一个。Navigator 导出是纯副作用,永不影响门禁。
MCP 与三层架构 —— 同一个治理闸门,既护住 AWS 生产面,也护住 Claude Code / Cursor 这类最不可信的边缘调用方。一份 registry,两个消费者,永不漂移。
文件系统发现工具,默认只暴露 17 个 —— 20 减 2 控制面减 1 pending
approved ∧ code-mapped,离线 tools.yaml 与 GA Registry 同一条规则
L1 策略 / L2 攻击验证 / L3 地基,逐层映射到 Harness 原语
大多数框架把 MCP 当垫片:手写 20 个 wrapper、20 份 JSON schema,然后永远手动同步。这里反过来 —— 启动时扫 tools/ 目录,谁没过治理闸门就不暴露。MCP 面和 AWS harness 面读同一个 registry,物理上无法漂移。
1 _discover_tools() 按字母序遍历 tools/,找每个目录的 handler.py。
2 逐目录过三道闸:治理闸 → 控制面闸 → import 加载。
3 两个 async handler 挂上:list_tools() 只报能加载的,call_tool() 取 event = arguments.get("event", arguments) 后调 mod.handler(event, None)。
默认暴露面:20 个目录 − 2 控制面 − 1 pending。这个算式在 tests/test_mcp_server.py:26 里被断言。
source checkout 里 command:"sentinel" 要求装在 PATH 上;否则用 uv run --extra mcp sentinel mcp serve。mcp SDK 是可选 extra,缺了 create_server() 会带安装提示 sys.exit(1),其余库照常工作。
连过来的 AI agent 在边缘是不可信的 —— 它看见什么就敢调什么。若 MCP 暴露全部工具,就等于广播了 (a) SecOps 从没批准的工具、(b) 会创建/删除真实 AWS 资源或烧 token 的控制面工具。所以复用 registry.py 当唯一裁判,不搞第三份 allowlist。
mcp_server.py:115if approved and tool_name not in approved and not allow_pending: continue
只有 status==approved 才放行,web_search(pending)被藏。
mcp_server.py:119_CONTROL_PLANE_TOOLS = {harness_ops, run_evaluation}
能建/删 AWS 或调模型(=花钱),默认 OFF
import 期报错的工具不静默丢弃:保留 module=None + [LOAD ERROR],从 list_tools 里滤掉,但 call_tool 返回 load_error —— 失败可见,不静默。
在真实 Claude Code 配置里开 ALLOW_PENDING=1 会悄悄放出 web_search(出网工具)。它 pending 恰恰因为出网 allowlist 未批准 —— 这是开发逃生舱,不是生产开关。缺 registry 时 _load_approved_set() 返回空集,故意 fail-open 让服务能起来而非崩溃。
一行 YAML,一个 Python dict key。两边都同意,工具才是活的。删 YAML —— SecOps 无需碰代码就吊销了;删 factory-map key —— 工具注册了却没实现,governance_check() 标为漂移、CI 挂掉。任何一边都无法单独让能力成真。
批准但未实现的名字无法 resolve(无 factory → raise),过期批准不会 500 掉 agent;已实现但未批准的影子能力永不可解析,且被 impl_missing_registry 曝光 —— 工程无法偷偷上一个 SecOps 没见过的工具。
governance_check()(registry.py:195)产出:live / approved_missing_impl / impl_missing_registry / pending / deprecated_with_code。report.ok 仅当两边都无漂移为 True —— pending 是故意 held,deprecated 却仍有 live factory 才是漂移。纯离线 · 零 AWS · 零 LLM
蓝图称 Gateway 是"最重要的可靠性升级":supervisor harness 去调一个工具,而不是自己写 HTTP。gateway.py 几乎每行都写着一个 offline mock 抓不到、只有真实 AWS 才报的 ValidationException,被编码成 fail-fast 的本地 guard。
CreateGateway 上没有原生 "Guardrail 拦截器" 原语。守护栏要么跑在 Lambda 拦截器里(ApplyGuardrail,interceptor.lambda.arn,作用于 REQUEST/RESPONSE),要么走独立的 policyEngineConfiguration(Bedrock guardrail ARN,LOG_ONLY / ENFORCE)。两条路互相独立。
live 证据 live_custom_jwt_gateway_result.json:
valid_jwt → 200 no_token → 401 garbage → 401
随后真实 Lambda MCP target(cve-severity-tool)端到端跑通,返回 {cvss:9.8, severity:critical}。机器 client secret 全程留在服务端(GenerateSecret=true),从未被取出或打印。
踩过的坑:Gateway 名字正则(≤48 字符、无尾连字符)≠ target 正则(≤100、允许尾连字符);payloadFilter.exclude 必须是结构体 {"field": <jsonpath>} 不是裸字符串;update_gateway_target 是全量替换不是 patch;所有 list 操作走 _drain_pages 排干每一页(只读第一页会漏删计费资源)。
核心洞见(ARCHITECTURE.md:5):安全团队已经有模型、MCP server、skills —— 缺的是让它们流动的框架。三层就是这条循环,自底向上组合:L3 决定工具面,L1/L2 消费它。
Gateway(L3) 是工具入口;registry(L3) 决定其中哪些是 live;L1 harness 调 tool_gateway 触达;L2 Runtime 调同一个 Gateway,但每步进攻都过 inline 闸。
独立 adversarial-reviewer 是单独一个 harness(生成≠评估,无自批偏差);tool_inline(core.py:507)暂停 loop(stop_reason=tool_use)把调用交回你的代码,等分析师签字。
端到端已验证的是 L1:CVE triage(live_cve_demo,78.6s / 78459 token 真实 Sonnet)、多 harness 并行、detection 生成+审阅+发布闸。L2/L3 为已设计、部分构建 —— 不夸大成全 live。
安全治理与部署 —— 从提示词是「建议」,到 IAM 与网络边界是「强制」
沙箱钩子 / SSRF / 禁 *
$PREFIX 空串陷阱
零常驻成本合成
tag 触发才发布
本章讲一件事:任何安全控制,只要还依赖「模型选择遵守」,注入就能击穿它。所以我们把执行点全部移到模型够不到的地方——确定性代码、IAM、以及物理上不可路由的网络。
每条控制要么是确定性代码、要么是 IAM/网络边界。没有一条依赖模型「决定守规矩」——因为 CVE 文本、SIEM 字段、网页片段都可能夹带注入载荷。
validate_command 五步 fail-closed:非空可解析 → 全串黑名单正则 → 链操作符拒绝 → 首动词白名单(42 个读/构建/测试/VCS 动词) → 逐参数路径限定。
validate_path 在归一化之前按字面拒绝 ..,于是 /workspace/../etc 被拦;兄弟目录 /workspace-evil 也因 norm==base 或 startswith(base+sep) 被拒。
test_sandbox_hooks.py test_fuzz_sandbox_hooks.py
create_from_config 使用前先校验形状:必须是 list(裸标量会逐字符迭代、静默漏接 HITL 门),每项非空字符串,出现 '*' 直接 ValueError('ironclad rule #1')。
嵌套 list / dict / None 也一并拒绝——非字符串工具名永远匹配不上,其 HITL 门会被静默地永不注入。
loader.py:266 · grant-all forbidden
_assert_safe_url 在任何实网请求打开之前跑:scheme 白名单 {https,http} 挡掉 file/gopher/ftp;IP 字面量做段范围检查,link_local/multicast/reserved/unspecified 全拒——169.254.169.254 元数据 IP 命中 is_link_local。
环回 127.0.0.1 故意允许:本机自托管搜索后端是合法运维选择;真正的威胁是元数据与链路本地地址。
test_ssrf_guard.py · 25+ 用例
''.startswith('') 是 True —— 一个未设置的 $PREFIX 删光整个账号sentinel cleanup $PREFIX 删掉所有名字以 PREFIX 开头的 harness,并级联删掉它们的 managed memory。触发者通常不是恶意,而是 teardown 脚本里一个没赋值的环境变量。单一护栏足够正确,三层是纵深——没有任何代码路径能带着空串抵达销毁循环。
cmd_cleanup:if not args.prefix.strip() → 打印拒绝、return 2,在任何破坏动作之前。
cli.py:226
安全彩排:只列出「会删哪些」而不碰任何东西——[dry-run] N harness(es) would be deleted。
cli.py:230
即便绕过 CLI 直接调库,core.cleanup 首行就 raise ValueError。
core.py:576
代价提醒:级联删除会连 triage 事实一起毁;且逐个删是 best-effort(catch/log/continue),部分失败不回滚——已删的保持已删。
出网是物理不可路由而非被过滤——没有 IGW/NAT/公网路由。guardrail 把 policy 的 sha256 嵌进版本描述,否则原地收紧策略会静默 no-op(一个被审计出的 bug)。Registry/Runtime 的未注册状态在各栈头如实写明,不藏。
bedrock-agentcore:InvokeAgentRuntimeCommand 以 root 在 microVM 上跑 shell,绕过 LLM 和 allowedTools。allowedTools 只约束 LLM 选哪个工具,管不了这个独立数据面 API——唯一控制就是不给这个 IAM action(ADR-0001 不变量 #6)。
执行角色的信任策略必须允许 Principal Service: bedrock-agentcore.amazonaws.com 做 sts:AssumeRole。
阻塞点是信任策略而非权限:一个权限完美但信任主体错误的角色照样失败。
用 AdministratorAccess 主体,iam:SimulatePrincipalPolicy 对 InvokeHarness 返回 allowed,实调却 AccessDeniedException。
根因是服务侧账号级 allowlist,IAM 模拟看不见。控制面(CreateHarness→READY)完全可用,只有 invoke 数据面被门控。别信绿色的模拟。
其余最小权限:观测权限用 cloudwatch:namespace='bedrock-agentcore' 条件限定而非 logs:*,避免 agent 被攻陷后全账号篡改日志;信任策略带 aws:SourceAccount=stack.account 挡跨账号混淆代理;X-Ray 因不支持该条件而单列一条语句(否则会静默破坏授权)。
这就是全书的模式:每个控制都能追溯到一个被抓到的具体缺陷。gate 存在,是因为审计发现 ci.yml 只在 branch 上触发、不在 tag 上跑——不加它,git tag v1.2.3 <任意 commit> 就能把未验证代码直发 PyPI。
tag 可以被移动,commit SHA 不能。被攻陷的 action tag 注入不进来。每条防线都对着一类具体威胁。
每个 action 钉到完整 40 位 SHA,上方带人类可读版本注释:actions/checkout@9c091bb... # v7.0.0
为什么:OpenSSF Scorecard 的 pinned-dependencies 检查因此加分;被劫持的可变 tag 无法悄悄换掉底层代码。
codeql.yml 每周扫 python + js-ts。pip-audit 阻断式,但只审项目自己 pyproject.toml 声明的依赖。
为什么只审自家依赖:审整个 CI venv 会被 runner 基线包误报而假红。bandit 顾问式 || true 不 gate。
覆盖三个生态:pip / npm / github-actions——连 action 的 SHA 升级都自动化提 PR。
为什么:钉死 SHA 不等于不升级;Dependabot 负责让钉死的版本持续追上安全补丁。
拦客户名(字符类拼装的正则)、硬编码 12 位 account、AKIA/ASIA 密钥;占位符(000000000000/1234.../5555...)白名单放行。
为什么把 .github 也纳入:那正是 live OIDC role ARN 最可能泄漏的地方。
纵深防御闭环:从「提示词是建议」到「IAM 与网络是强制」,每一层都留了署名证据——OpenSSF Scorecard 徽章经 OIDC 发布,供任何人复核。
前面七章讲的是设计与取舍。这一章只做一件事:把每一条声明还原成一份可以独立攻击的真实证据——真账号、真 ARN、真 token 计数、真模型答案。
压轴的方式不是 PPT,是 evidence/*.json。一个怀疑一切的安全架构师,只信落地的工件。
一个一分钟前还不存在的 harness,在真实 AWS 上生出来、答完题、再自己删掉。四步账本,闭环干净。
75k 输入 token 是关键 tell——答题前先把 NVD 与 MITRE/CISA 两个权威源拉进上下文,这是有据的 grounding,不是幻觉长文。
CVSS 10.0 CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
根因:log4j2 消息 lookup 把攻击者可控串经 ${jndi:ldap://…} 直送 JNDI 解析器;LDAP 回引远程类,JVM 取回并实例化 → 进程内 RCE。
CWE:CWE-917(主)· CWE-502 · CWE-20 KEV:2021-12-10 收录
修复深度:准确区分 2.16.0(45046)/2.17.0(45105)/2.17.1 才是最小安全版——一道事实深度体检,答案通过。
刻意拆成「一能力一文件」——评审可以逐条独立进攻,任何一步 flaky 都不会拖垮全局。
弱 agent 被独立 judge 打 0.0 → update_harness 换强 prompt(v2) → 复评 1.0 → 过 safety-veto + regression-guard → HITL approve → CreateHarnessEndpoint。reject 路径证明会扣住晋升。
tenant-1 四个会话写入 web-01 Log4Shell 处置,语义策略异步抽取,跨会话检索回 3 条;同一 memory、同 query,tenant-2 回 0 条。硬隔离。
真建 Cognito user pool + M2M client_credentials,Gateway 用 CUSTOM_JWT authorizer 指向 OIDC discovery。带合法 RS256 token → 200;无/错 token → 401。
turn1 停在 request_containment_approval(WEB-07 isolate,T1071+T1003.001)→ 人类 analyst-001 批准 → turn2 end_turn。两消息 resume 契约成立。
Log4Shell(CVSS 10.0, EPSS 0.975, in KEV) → 命中资产 web-01 → blast radius 触及 app-01、internet_exposed 命中 → 建议 patch_now,且 强制 HITL 签核后才动手,无 AI 自动执行。
这不是口号,是被文件布局、脱敏正则、CI secret-scan 和每份 JSON 里的 honesty 字段一起强制执行的规矩。最有说服力的,恰恰是它写清了自己不做什么。
每个 scenario_*.py 用 rec() 追加 {step, ok, data},_ACCT_RE 把 12 位账号脱敏为 <ACCOUNT_ID> 后再落盘;顶层 verdict 带 closed 布尔 + note 明说哪些真、哪些 mock。
FIDELITY-REPORT.md 敢用 GENUINE 开头:凡标 live/real,无一暗中 mock。
① 模拟 vs 真:BAS 遥测是模拟 Sysmon 事件,但匹配/回放是真确定性 Python。
② quota-gated:M2 单次 re-score 撞过 InvokeHarness 403,晋升在 endpoint_promote 里单独证。
③ runner vs agent 编排:loop 决策由 runner 编排,agent 自编排变体仅离线脚本,不声称 live。
④ 异步 memory:语义抽取由服务调度(分钟级),无 force-now。
连「诊断错了」都留档:live_dataplane_gate_diagnosis.json 被更正后保留——10 分钟一个鉴别性测试,胜过百万 token 的推断扫。verify, don't infer.
「配置而非代码」的前提,是那层薄核心库必须刀枪不入。~127 个测试文件、约 2501 条用例,工程上被设计成密封的。多轮对抗审计累计修掉约 100 个真实缺陷。
conftest.py:30-43 在 import 期 pop 掉 AWS_PROFILE,setdefault 假凭证 + us-east-1——任何测试都碰不到真账号。
@given 攻击确定性 gate 核心:blast-radius / sigma-match / whitelist……断言不变量(幂等、count 相等、可达与受影响不相交),非同义反复。
tomllib<3.11、bash 不可用、PyYAML 缺失、CDK node_modules 缺——每个都有显式 reason,绝无静默失败。
contract 测试(test_specialist_a2a_contract.py)对两个 specialist 参数化跑同一断言:agent-card=canonical、message/send 往返结构化信封、畸形输入被拒。pytest-randomly 打印种子、顺序无关;coverage gate 88%(.coveragerc 与 ci.yml 共享)。
只有 4 个 Makefile target 碰 AWS(deploy/deploy-endpoints/reset/destroy),每个都先打印账号+region 再要你键入 yes。让新人先「看它跑起来」,再「为它花钱」。
standing 成本 ≈ $0/mo;唯一有意义的是可选 make deploy-endpoints(~$30/mo PrivateLink)。
github.com/aws-samples/sample-sentinel-harness pip install sentinel-harness
安全 agent 是配置,不是编排代码。
AWS 跑循环,证据替你说话。