Harness 与治理
用运行环境控制工具、权限、副作用、预算、人工确认、恢复和平台差异。
谁控制权限、运行环境、副作用、平台适配和恢复?概念校准
Harness
- 它是什么
- 承载模型、工具、状态、权限、日志、预算、人工确认和恢复的运行控制环境。
- 它不是什么
- 它能限制风险并保留证据,但不能直接阻止模型产生幻觉。
工具
- 它是什么
- Agent 可通过明确参数、权限和结果合同调用的受约束能力。
- 它不是什么
- 工具不等于 MCP;本地函数、命令或 HTTP 客户端也可以是工具。
Skill
- 它是什么
- 包含指令、资源、适用边界、版本和兼容信息的可复用能力包。
- 它不是什么
- 不等于一段 Prompt,也不意味着其中脚本可以被无条件执行。
身份、权限、安全与供应链
覆盖身份、授权、最小权限、人工确认、注入攻击、依赖风险和事故响应。
Python 3.12 · pytest · Pydantic · SQLite · deterministic Mock概念校准\n\n在开始操作代码前,先把本章涉及的概念放回正确的工程边界。\n\n\n\n### Harness harness\n\n- 它是什么: 承载模型、工具、状态、权限、日志、预算、人工确认、隔离和恢复的运行控制环境。\n- 它不是什么: 它可以缩小风险、拒绝动作并保留证据,但不能保证模型输出永远正确,也不能代替领域验收。\n- 与相邻概念的关系: Agent Loop 在 Harness 中运行;Tool、Memory 和 MCP 连接受其策略约束;Observability 记录过程,Evaluation 判断行为。\n- 在 HeatStack Forge 中的位置: Forge Harness 管理工作目录、工具、确认队列、预算、Checkpoint、审计日志和回滚,把模型建议与真实副作用分开。\n- 典型误用与修正: 误用是把系统提示词当成安全边界。修正方法是把权限、路径、网络、密钥、预算和回滚落实在模型之外的控制中。\n\n### 工具 tool\n\n- 它是什么: Agent 可通过明确参数、权限、执行边界和结果合同调用的受约束能力,例如函数、命令、数据库查询或 HTTP 客户端。\n- 它不是什么: 工具不等于 MCP,也不等于任意脚本。协议可以暴露能力,但 Harness 仍决定是否允许调用及如何处理副作用。\n- 与相邻概念的关系: Agent Loop 选择工具,Schema 约束参数,Harness 执行权限检查和隔离,Verification 检查结果是否推进目标。\n- 在 HeatStack Forge 中的位置: Forge 的 tool registry 保存合同与风险等级;执行前生成变更计划,高风险写操作等待确认,执行后记录结果与恢复信息。\n- 典型误用与修正: 误用是让模型根据工具名称自由拼参数。修正方法是严格 Schema、确定性校验、超时、幂等键和拒绝分支测试。\n\n### Skill skill\n\n- 它是什么: 包含指令、资源、适用边界、版本和兼容元数据的可复用能力包,可被 Agent 平台发现、安装或引用。\n- 它不是什么: 它不等于一段 Prompt,也不代表附带脚本可以自动执行;实际执行仍受平台、工具合同和权限策略约束。\n- 与相邻概念的关系: Prompt 可以是 Skill 的一部分,Tool 可以被 Skill 引用,Harness 决定安装与运行边界,适配器处理平台差异。\n- 在 HeatStack Forge 中的位置: Forge 将需求抽取能力打包为带 Manifest、测试、资源清单和风险说明的 Skill,并适配三个目标平台。\n- 典型误用与修正: 误用是复制一个 Prompt 文件就宣称跨平台兼容。修正方法是声明依赖、入口、资源、版本、权限与验收测试。\n\n
从受信任声明构造有时效的身份主体,并拒绝过期或缺少主体的凭据
先看一个具体问题
为什么过期凭据仍能通过认证?
当前 forge/security/identity.py 中的 authenticate 函数只检查了发行方和主体,没有检查过期时间,导致过期凭据被错误接受。
测试 test_expired_credential_is_rejected 期望当 exp 小于等于 now 时抛出包含 “expired” 的 PermissionError,但现有代码会直接返回 Principal 对象。
你需要补全过期时间检查,确保只有未过期的凭据才能创建主体。
先做判断
在 authenticate 函数中,如果只验证发行方和主体,不检查过期时间,会发生什么?
- 过期凭据会被拒绝,因为发行方正确
- 过期凭据会被接受,因为缺少过期检查
- 主体为空时会抛出异常
- 角色列表会被忽略
判断依据: 正确答案是“过期凭据会被接受,因为缺少过期检查”。认证必须同时验证声明来源和时效,否则过期令牌仍可创建主体。
它是什么
认证函数是一个时效性守卫
authenticate 接收声明字典、当前时间和受信任发行方,依次验证发行方、主体和过期时间,全部通过后才返回 Principal。
过期时间检查是最后一道防线,确保凭据在验证时刻仍然新鲜。
它不是什么
认证不授予任何操作权限
认证只确认声明来自受信任发行方且未过期,不决定该主体能执行哪些操作。
权限授权是后续阶段的任务,不能与身份验证混淆。
它与相邻概念的关系
声明、主体与过期时间的关系
声明字典中的 iss、sub、exp 和 roles 共同描述一个身份凭据。
Principal 对象是认证成功后的产物,包含主体、发行方和角色集合,但不包含过期时间,因为过期时间只在验证时使用。
本阶段的边界决定: 认证只验证声明来源和时效,不授予任何操作权限;过期检查必须使用 <= 而不是 <,确保 exp == now 时凭据已过期。
将角色、资源和动作绑定为显式策略,未匹配请求默认拒绝
先看一个具体问题
空策略导致危险操作被放行
当前 forge/security/authorization.py 文件尚未创建,测试在导入 Rule 和 authorize 时直接失败。
即使补上文件,如果 authorize 在规则为空时直接返回,test_missing_policy_is_denied 会断言失败,因为删除操作在没有策略的情况下被允许。
你需要实现一个授权函数:当没有规则匹配时,必须抛出 PermissionError,并且错误消息中包含 "default"。
先做判断
如果 authorize 收到空的规则元组,以下哪种行为最安全?
- 直接返回,表示没有限制
- 抛出
PermissionError,默认拒绝 - 记录警告但允许操作
- 返回
False表示拒绝
判断依据: 默认拒绝是安全基线,因为未明确允许的请求不应获得访问权限。抛出异常可以强制调用方处理拒绝情况。
它是什么
授权策略引擎
授权函数将主体、动作、资源与规则集合进行匹配,决定是否允许操作。
规则由角色、动作和资源前缀组成,只有三者同时匹配才视为允许。
当没有任何规则匹配时,函数必须抛出 PermissionError,实现默认拒绝。
它不是什么
不是身份验证或结果验证
授权只判断动作是否被允许,不验证主体身份的真实性,也不检查动作执行后的结果。
它不负责记录日志、审计或提供用户界面,这些属于其他组件。
它与相邻概念的关系
与 Principal 和 Rule 的关系
Principal 提供主体的角色集合,Rule 定义允许的角色、动作和资源前缀。
authorize 遍历规则,检查主体角色是否包含规则角色、动作是否相等、资源是否以规则前缀开头。
如果所有规则都不匹配,则抛出 PermissionError,确保未授权请求被拒绝。
本阶段的边界决定: 授权引擎只负责允许或拒绝,不负责执行动作或验证动作结果;默认拒绝是安全边界。
把人工确认绑定到具体主体、操作摘要和过期时间,防止确认被复用
先看一个具体问题
为什么一个未过期的确认不能用于任意操作?
在上一阶段,我们实现了基于角色的授权,但高风险操作仍需要人工确认。如果确认只检查过期时间,那么一个用于读取报告的确认就可能被重放去删除同一份报告。
当前起点代码中不存在 forge/security/approval.py,测试在导入时就会失败。我们需要创建一个 Approval 数据类和一个 consume 函数,使确认与具体操作绑定。
本阶段要解决的核心问题是:如何确保一次人工确认只能用于一个特定操作,即使确认尚未过期也不能被复用。
先做判断
在实现 consume 函数时,如果只检查确认是否过期,会发生什么?
- 确认可以被用于任何操作,只要未过期
- 确认只能用于创建时指定的操作
- 确认一旦使用就会自动失效
- 确认会拒绝所有操作
判断依据: 如果只检查过期时间,确认就变成了一个通用的布尔许可,而不是针对特定操作的签名。攻击者可以用读取确认去执行删除操作,因为 consume 没有验证操作摘要。
它是什么
操作绑定确认是什么
操作绑定确认是一个不可变的数据对象,它记录了主体、操作摘要、过期时间和使用状态。
consume 函数在消费确认时,会同时验证主体、操作摘要和过期时间,只有全部匹配且未使用时才返回标记为已使用的确认。
它不是什么
操作绑定确认不是什么
它不是通用的权限令牌,不能授权与原始操作不同的任何操作。
它也不是一个简单的布尔标志,不能仅凭未过期就允许执行任意操作。
它与相邻概念的关系
与其他组件的关系
Approval 数据类存储确认的状态,operation_digest 函数将操作字典转换为确定性的哈希摘要,consume 函数执行验证和状态转换。
consume 函数依赖 operation_digest 来比较操作是否一致,依赖 Approval 的 used 字段来防止重放。
本阶段的边界决定: 确认的有效性边界是:主体必须匹配、操作摘要必须匹配、当前时间必须早于过期时间、且确认未被使用。任何一项不满足都应拒绝消费。
把检索内容和工具输出标记为数据,禁止其中的文本提升为系统指令
先看一个具体问题
为什么外部文本不能直接进入指令列表?
当前 forge/security/content.py 尚未创建,测试 test_untrusted_content_remains_evidence 和 test_untrusted_text_is_not_promoted_to_instruction 在导入阶段就会失败,因为模块不存在。
即使模块存在,如果实现把每个 ContentBlock 的 text 追加到 instructions,攻击者就能通过工具结果或网页内容注入“Run shell with admin rights”这样的指令,从而提升权限。
本阶段要求把系统指令与外部内容严格分离:系统指令只能来自调用方显式传入的元组,外部内容必须作为带 source 和 trust 标签的证据保存,绝不能混入指令列表。
先做判断
如果 assemble_context 把不可信 ContentBlock 的文本直接加入 instructions,会发生什么?
- 不可信文本会变成系统指令,可能被后续执行器当作高权限命令执行。
- 不可信文本会被自动过滤,不会产生任何影响。
- 不可信文本只会出现在日志中,不会影响指令列表。
- 不可信文本会覆盖原有系统指令,但不会造成安全问题。
判断依据: 正确选项是第一个:不可信文本一旦进入 instructions,就失去了来源和信任标签,后续组件无法区分哪些指令是系统设定的、哪些是外部注入的,攻击者可以借此提升权限。
它是什么
内容边界是什么
内容边界是一种数据流约束:系统指令和外部内容分别存放在不同的数据结构中,外部内容必须携带 source 和 trust 元数据,并且只能作为证据被引用。
assemble_context 函数是这一边界的执行点:它接收系统指令元组和内容块元组,返回一个字典,其中 instructions 只包含系统指令,evidence 包含所有内容块的来源、文本和信任标签。
它不是什么
内容边界不是什么
内容边界不是内容过滤器,它不会判断外部文本是否恶意,也不会修改或丢弃文本;它只负责隔离,确保外部文本永远不会被当作指令执行。
内容边界也不是权限系统,它不决定哪些指令可以执行,只保证指令列表的纯净性;权限检查由其他组件完成。
它与相邻概念的关系
与其他安全组件的关系
内容边界位于工具输出和指令执行之间:工具返回的文本先被包装成 ContentBlock,然后通过 assemble_context 进入上下文,但只能出现在 evidence 中。
它与审批机制(Stage 03)配合:审批只针对系统指令,而证据用于展示和审计,两者分离可以防止外部文本绕过审批流程。
本阶段的边界决定: 当外部文本可能包含指令时,必须将其标记为不可信证据,而不是尝试解析或过滤其中的指令;任何试图从外部文本中提取指令的行为都会破坏边界。
验证包内路径、文件哈希和许可证声明,拒绝逃逸路径与篡改内容
先看一个具体问题
软件包文件可能被篡改或逃逸安装目录
你正在为 HeatStack Forge 实现软件包验证功能。当前 forge/security/supply_chain.py 文件尚不存在,测试 test_valid_package_passes 和 test_path_traversal_is_rejected 在导入模块时就会失败。
测试要求 verify_package 函数接收文件字典、预期哈希字典和许可证标识符,并验证每个文件的 SHA-256 哈希是否匹配,同时拒绝任何可能逃逸安装根目录的路径。
如果只验证哈希而忽略路径,攻击者可以提供一个哈希完全正确的 ../outside.txt 文件,导致解包时写入安装目录之外,破坏系统安全。
先做判断
在实现 verify_package 时,以下哪种做法能同时满足哈希验证和路径安全?
- 只验证每个文件的 SHA-256 哈希是否与预期一致。
- 验证哈希后,再检查路径是否为绝对路径或包含
..片段。 - 只检查许可证标识符是否非空,哈希和路径都交给调用方处理。
- 先解包文件到临时目录,再验证哈希,最后移动文件。
判断依据: 正确选项是第二个:哈希验证只能保证内容未被篡改,但无法防止路径逃逸。必须显式检查路径是否为绝对路径或包含父目录片段 ..,否则攻击者可以利用 ../ 将文件写入任意位置。
它是什么
供应链验证是完整性检查与路径安全约束的组合
供应链验证通过计算每个文件内容的 SHA-256 哈希并与预期值比较,确保文件在传输或存储过程中未被篡改。
同时,它使用 PurePosixPath 解析路径,拒绝绝对路径和包含 .. 片段的路径,从而保证所有文件都位于包根目录内。
许可证标识符必须非空,这是对包元数据的基本要求,防止未声明许可证的包被意外分发。
它不是什么
供应链验证不评估代码质量或功能正确性
供应链验证只关心文件是否完整且路径安全,它不会检查代码是否存在 bug、是否符合编码规范或是否实现了预期功能。
它也不是权限系统:即使文件通过验证,后续执行时仍需要 Harness 根据权限策略决定是否允许运行。
哈希匹配不代表文件来源可信,它只能证明文件与创建哈希时的内容一致,无法防止初始内容本身包含恶意代码。
它与相邻概念的关系
路径检查、哈希验证和许可证检查相互独立但共同构成验证流程
许可证检查最先执行,因为缺少许可证的包没有继续验证的必要;哈希验证和路径检查可以按任意顺序进行,但通常先检查路径以避免对逃逸文件做无意义的哈希计算。
PurePosixPath 提供跨平台的 POSIX 路径解析,即使代码运行在 Windows 上也能正确识别 .. 和绝对路径,保证验证逻辑的一致性。
expected_hashes 的键集合必须与 files 的键集合完全一致,否则说明清单与包内容不匹配,应直接拒绝。
本阶段的边界决定: 供应链验证的边界是:只验证完整性和路径安全,不评估代码质量或功能正确性。如果包内容本身包含恶意代码但哈希正确且路径安全,验证仍会通过,因此后续还需要其他安全机制(如代码审查、沙箱执行)来降低风险。
在结构化日志中递归清理密钥字段,同时保留可诊断的非敏感上下文
先看一个具体问题
结构化日志中的嵌套密钥泄露
在上一阶段,我们建立了供应链验证,但还没有遥测模块。现在需要创建一个 forge/security/telemetry.py,提供 redact 函数,用于在记录结构化日志前清理敏感字段。
初始代码不存在,因此测试 test_top_level_secret_is_redacted 和 test_nested_secret_is_redacted 会因导入错误而失败。
如果只清理顶层字段,嵌套在 request.headers 中的 Authorization 头会原样输出,导致 Bearer 令牌泄露。
本阶段的目标是递归处理字典、列表和元组,将所有已知敏感键的值替换为 <redacted>,同时保留非敏感上下文。
先做判断
在实现 redact 之前,请预测:如果只对顶层字典的键进行判断,嵌套字典中的敏感字段会发生什么?
- 嵌套敏感字段会被自动清理,因为 Python 会递归处理。
- 嵌套敏感字段不会被清理,仍然保留原始值。
- 程序会抛出异常,因为无法处理嵌套结构。
- 只有列表中的敏感字段会被清理,字典中的不会。
判断依据: 正确答案是第二个选项。如果只遍历顶层字典,嵌套字典的值不会被检查,因此其中的敏感字段会原样保留。这会导致日志泄露,需要通过递归来解决。
它是什么
递归脱敏是什么
递归脱敏是一种遍历任意嵌套数据结构(如字典、列表、元组)的算法,对每个键检查是否属于敏感集合,若是则替换其值为 <redacted>,否则继续递归处理其值。
它确保无论敏感字段位于多深的层级,都会被统一清理,同时保留非敏感字段的原始值以供诊断。
它不是什么
递归脱敏不是什么
它不是一个能识别任意格式秘密的检测器,只处理预定义的敏感键集合,例如 authorization、api_key、token、password、secret。
它不会修改原始数据结构,而是返回一个新的脱敏副本,因此调用方可以安全地记录结果而不会影响原始事件。
它与相邻概念的关系
与其他组件的关系
redact 函数将被遥测模块调用,在日志输出前清理事件数据。它与供应链验证模块独立,但共同构成安全防线。
测试文件 tests/test_stage.py 直接导入 redact 并验证顶层和嵌套场景,确保脱敏逻辑正确。
本阶段的边界决定: 脱敏只处理已知敏感键,不识别未知格式的秘密;因此,如果攻击者使用自定义字段名存储令牌,该函数无法拦截。
用显式事故状态机记录发现、隔离、修复与恢复,阻止跳过隔离直接关闭事故
先看一个具体问题
为什么不能直接标记为已恢复?
在真实事故响应中,如果检测到安全事件后直接标记为“已恢复”,就会跳过隔离和修复步骤,导致系统仍然暴露在风险中。
当前代码库中还没有事故模块,测试文件 tests/test_stage.py 中的 test_valid_incident_lifecycle 和 test_recovery_cannot_skip_containment 会因导入失败而报错。
你需要创建 forge/security/incident.py,实现一个显式状态机,确保事故只能按照“检测→隔离→修复→恢复”的顺序推进。
先做判断
如果 transition 函数只检查证据文本非空,而不检查目标状态是否合法,会发生什么?
- 事故可以跳过隔离直接恢复,测试
test_recovery_cannot_skip_containment会失败。 - 事故仍然会按照正确顺序推进,因为证据文本保证了顺序。
- 状态机会自动执行隔离和修复动作。
- 只有缺少证据时才会失败。
判断依据: 正确答案是第一个选项。证据文本只能说明有人记录了操作,但不能保证操作顺序正确。状态机必须显式检查当前状态到目标状态的转换是否在允许的图中,否则测试会捕获到跳过隔离的非法转换。
它是什么
显式事故状态机
事故状态机是一个有向图,节点是事故状态(detected、contained、remediated、recovered),边是允许的转换。
transition 函数接收当前事故、目标状态和证据,首先检查目标状态是否在当前状态的允许转换集合中,然后检查证据非空,最后返回一个带有新状态和追加证据的新事故对象。
它不是什么
状态机不做什么
状态机不会自动执行隔离或修复动作,它只负责记录状态转换并拒绝非法顺序。
状态机不是审计日志的替代品,它只约束流程顺序,不保证每个步骤都真正执行了安全操作。
它与相邻概念的关系
与其他安全组件的关系
事故状态机与遥测脱敏(Stage 06)配合:遥测提供检测证据,状态机确保响应流程有序。
状态机是权限策略和审批流程的基础:只有状态合法推进,后续的审批和审计才有意义。
本阶段的边界决定: 状态机只约束流程顺序,不自动执行隔离或修复动作;它通过拒绝非法转换来防止跳过关键步骤,但实际的安全操作仍需其他工具或人工完成。
完成本章
建立权限策略、风险报告、供应链检查和高风险操作确认。
本地实验自检
- 未开始
- 2阅读中
- 3实验已下载
- 4测试结果已读取
- 5本地自检通过
verification.json 只在当前浏览器中解析,不会上传。这里验证的是实验合同,不是服务器认证或第三方背书。
概念校准与一周复习
三道题检查你是否掌握了本章边界、交付证据和恢复方法。答案只保存在当前浏览器。