
Harness 约束层实现:为 AI Agent 构建安全可控的执行环境
三层防御体系 + 动态规则引擎 + Docker 沙箱,让 Agent 在安全边界内自由行动
📑 目录
一、引言:AI Agent 的安全悖论
AI Agent 越强大,带来的安全风险就越大。
一个能够读写文件、执行命令、调用 API、访问数据库的 Agent,如果缺少有效的约束机制,就像一台没有刹车的跑车——速度越快,风险越高。
现实中的 Agent 安全事故:
- Agent 在执行“清理临时文件”任务时,误删了生产环境的关键配置文件
- Agent 在调试代码时,执行了
rm -rf /命令(虽然被拦截了,但暴露了风险) - Agent 在读取数据库时,返回了包含用户敏感信息的查询结果
- Agent 在调用第三方 API 时,因循环调用导致费用失控
这些问题不是模型能力不足,而是缺少一层有效的约束机制。
Harness 的约束层正是为此而生。它像一道安全过滤器,位于“决策引擎”与“工具执行”之间,确保 Agent 的每一个行动都在安全边界内。
二、Harness 在 Uni-MDP 中的落地位置
在 Uni-MDP 的整体架构中,Harness 约束层处于算法层(决策引擎) 与中台层(工具执行) 之间:
约束层在决策链路中的位置:
三、三层约束体系设计
3.1 认知约束
定位:通过 System Prompt 注入行为准则,让 Agent 在“思考”阶段就知道什么该做、什么不该做。
认知约束是最前置的约束层,在 Agent 生成决策之前就已经生效。
在 Uni-MDP 中的实现形式:
# AGENTS.md - 认知约束配置文件
# 定义 Agent 的行为准则
## 角色定义
你是一个营销决策助手,职责包括:
- 分析用户行为数据,提供营销建议
- 执行营销活动配置
- 生成用户分群和标签
## 行为准则
1. 在执行任何数据写入操作前,必须先确认影响范围
2. 在涉及用户隐私数据时,必须脱敏处理
3. 预算相关的操作必须经过二次确认
4. 批量操作必须限制在合理范围内(单次不超过 10000 条)
## 安全边界
- 禁止删除用户数据
- 禁止修改系统配置
- 禁止执行未授权的数据库操作
- 禁止调用未列入白名单的 API
3.2 权限约束
定位:工具调用前的权限校验——Agent 能调用哪些工具、能访问哪些资源、能操作哪些数据。
权限模型的层级结构:
3.3 流程约束
定位:标准化执行流程,确保 Agent 的行动遵循“规划→执行→验证→反馈”的标准路径。
流程约束的校验点:
| 校验点 | 检查内容 | 拒绝条件 |
|---|---|---|
| 计划完整性 | 是否包含所有必要步骤 | 缺少关键步骤 |
| 步骤依赖 | 步骤之间的依赖关系是否正确 | 存在循环依赖 |
| 资源准备 | 执行所需的资源是否就绪 | 缺少必要权限或资源 |
| 影响评估 | 操作的影响范围是否可控 | 影响范围超出授权 |
| 结果验证 | 执行结果是否符合预期 | 结果异常或未达预期 |
四、Java 实现方案
4.1 规则引擎选型与实现
在约束层中,规则引擎负责执行权限校验和流程约束。我们对比了几种主流方案:
| 规则引擎 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Drools | 功能强大、支持复杂规则、DRL 语言灵活 | 重量级、学习曲线陡峭 | 复杂业务规则场景 |
| Aviator | 轻量级、高性能、表达式简洁 | 不支持复杂规则编排 | 简单表达式计算 |
| EasyRules | 简单易用、注解驱动 | 功能相对有限 | 规则数量较少的场景 |
| 自研表达式 | 完全可控 | 开发成本高 | 特殊定制需求 |
在 Uni-MDP 中,我们采用 Aviator + 规则模板 的组合方案:
- Aviator:负责执行具体的约束判断表达式
- 规则模板:将约束规则抽象为可配置的模板,业务人员可动态调整
package com.unimdp.harness.rule;
import com.googlecode.aviator.AviatorEvaluator;
import com.googlecode.aviator.Expression;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
public class RuleEngine {
// 规则缓存:避免重复编译
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
/**
* 执行规则判断
*/
public RuleResult evaluate(String ruleId, Map<String, Object> context) {
try {
// 1. 获取规则定义
RuleDefinition rule = getRuleDefinition(ruleId);
if (rule == null) {
return RuleResult.pass("Rule not found, default pass");
}
// 2. 编译或获取缓存的表达式
Expression expression = expressionCache.computeIfAbsent(
rule.getExpression(),
expr -> AviatorEvaluator.compile(expr, true)
);
// 3. 执行表达式
Object result = expression.execute(context);
// 4. 解析结果
if (result instanceof Boolean) {
boolean passed = (Boolean) result;
return passed
? RuleResult.pass("Rule passed: " + rule.getName())
: RuleResult.fail(rule.getFailMessage(), rule.getFailAction());
}
return RuleResult.pass("Rule result is not boolean, default pass");
} catch (Exception e) {
log.error("Rule evaluation failed: {}", ruleId, e);
// 默认拒绝(安全优先)
return RuleResult.fail("Rule evaluation failed", FailAction.BLOCK);
}
}
}
规则定义的数据结构:
@Data
@Builder
public class RuleDefinition {
private String id;
private String name;
private String description;
private String expression; // Aviator 表达式
private String failMessage;
private String failAction; // BLOCK / WARN / LOG_ONLY
private Integer priority;
private Boolean enabled;
private List<String> tags;
}
// 规则配置示例
// 规则:禁止删除操作
// expression: toolName == 'delete' ? false : true
// failMessage: 删除操作已被禁止,如需执行请联系管理员
// failAction: BLOCK
4.2 权限校验拦截器
package com.unimdp.harness.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class PermissionInterceptor implements HandlerInterceptor {
@Autowired
private RuleEngine ruleEngine;
@Autowired
private AuditService auditService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 提取调用上下文
ToolCallContext context = extractContext(request);
// 2. 执行权限校验
String permissionRule = getPermissionRule(context.getToolName());
RuleResult result = ruleEngine.evaluate(permissionRule, context.toMap());
// 3. 记录审计日志
auditService.logPermissionCheck(context, result);
// 4. 处理校验结果
if (!result.isPassed()) {
log.warn("Permission denied: tool={}, user={}, reason={}",
context.getToolName(),
context.getUserId(),
result.getMessage()
);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getWriter().write(buildErrorResponse(result));
return false;
}
return true;
}
private ToolCallContext extractContext(HttpServletRequest request) {
// 从请求中提取调用上下文
return ToolCallContext.builder()
.userId(request.getHeader("X-User-Id"))
.sessionId(request.getHeader("X-Session-Id"))
.toolName(request.getHeader("X-Tool-Name"))
.params(extractParams(request))
.build();
}
}
4.3 结果验证器链
package com.unimdp.harness.validator;
@Component
@Slf4j
public class ResultValidatorChain {
private final List<ResultValidator> validators;
public ResultValidatorChain() {
this.validators = new ArrayList<>();
this.validators.add(new SchemaValidator()); // 格式验证
this.validators.add(new RangeValidator()); // 范围验证
this.validators.add(new SecurityValidator()); // 安全验证
this.validators.add(new BusinessValidator()); // 业务验证
}
/**
* 执行验证链
*/
public ValidationResult validate(ToolCallContext context, Object result) {
for (ResultValidator validator : validators) {
ValidationResult vr = validator.validate(context, result);
if (!vr.isPassed()) {
log.warn("Validation failed: validator={}, reason={}",
validator.getName(), vr.getMessage());
return vr;
}
}
return ValidationResult.pass();
}
}
// 验证器接口
public interface ResultValidator {
String getName();
ValidationResult validate(ToolCallContext context, Object result);
}
// 安全验证器示例
@Component
@Slf4j
public class SecurityValidator implements ResultValidator {
@Override
public String getName() {
return "SecurityValidator";
}
@Override
public ValidationResult validate(ToolCallContext context, Object result) {
// 检查返回结果是否包含敏感信息
String jsonResult = toJson(result);
if (containsSensitiveData(jsonResult)) {
return ValidationResult.fail(
"Return result contains sensitive data (email/phone/idcard)"
);
}
return ValidationResult.pass();
}
private boolean containsSensitiveData(String json) {
// 检测手机号、身份证、邮箱等敏感信息
return Pattern.matches(".*\\d{11}.*", json) ||
Pattern.matches(".*\\d{17}[\\dXx].*", json) ||
Pattern.matches(".*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}.*", json);
}
}
五、沙箱隔离
5.1 Docker 容器化执行
5.2 Java 实现
package com.unimdp.harness.sandbox;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.ResourceLimits;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class DockerSandbox {
@Autowired
private DockerClient dockerClient;
public SandboxResult execute(ToolCallContext context, String command) {
// 1. 创建容器
CreateContainerResponse container = dockerClient.createContainerCmd("unimdp-agent-sandbox:latest")
.withName("sandbox-" + System.currentTimeMillis())
.withCmd("/bin/sh", "-c", command)
.withHostConfig(HostConfig.newHostConfig()
.withMemory(2 * 1024 * 1024 * 1024L) // 2GB
.withMemorySwap(0L) // 禁用 swap
.withCpuCount(2L) // CPU 上限 2 核
.withReadonlyRootfs(true) // 只读根文件系统
.withNetworkMode("none") // 默认无网络
.withBinds(new Bind("/tmp/sandbox", new Volume("/workspace")))
)
.exec();
try {
// 2. 启动容器
dockerClient.startContainerCmd(container.getId()).exec();
// 3. 等待执行完成
WaitResponse waitResponse = dockerClient.waitContainerCmd(container.getId())
.start()
.awaitCompletion(30, TimeUnit.SECONDS);
// 4. 获取日志
String log = dockerClient.logContainerCmd(container.getId())
.withStdOut(true)
.withStdErr(true)
.exec()
.readFully();
// 5. 返回结果
return SandboxResult.builder()
.exitCode(waitResponse.getStatusCode())
.output(log)
.build();
} catch (Exception e) {
log.error("Sandbox execution failed", e);
return SandboxResult.error(e.getMessage());
} finally {
// 6. 清理容器
dockerClient.removeContainerCmd(container.getId())
.withForce(true)
.exec();
}
}
}
5.3 资源限制配置
# sandbox-config.yaml
sandbox:
docker:
image: unimdp-agent-sandbox:latest
memory-limit: 2GB
cpu-limit: 2
disk-limit: 10GB
timeout-seconds: 30
network:
mode: none # none / bridge / host
allowed-hosts:
- api.openai.com
- api.anthropic.com
read-only-mounts:
- /etc
- /usr
- /bin
writable-mounts:
- /workspace
- /tmp
banned-commands:
- rm -rf /
- dd if=
- mkfs
- chmod 777
- sudo
六、审计日志
6.1 审计日志数据模型
CREATE TABLE audit_logs (
log_id BIGINT PRIMARY KEY AUTO_INCREMENT,
-- 审计主体
user_id VARCHAR(64) NOT NULL,
session_id VARCHAR(64) NOT NULL,
agent_id VARCHAR(64),
-- 操作信息
action_type VARCHAR(32) NOT NULL, -- TOOL_CALL / SYSTEM_CMD / DATA_ACCESS
tool_name VARCHAR(64),
action_params JSON,
action_result JSON,
-- 决策信息
decision_path JSON, -- 完整决策路径(含推理链)
constraint_type VARCHAR(32), -- 认知/权限/流程
constraint_check VARCHAR(128), -- 具体约束规则
-- 执行结果
status VARCHAR(16) NOT NULL, -- SUCCESS / FAILED / BLOCKED
error_message TEXT,
-- 技术元数据
ip_address VARCHAR(45),
user_agent TEXT,
request_id VARCHAR(64),
-- 时间
executed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 索引
INDEX idx_user_id (user_id),
INDEX idx_session_id (session_id),
INDEX idx_action_type (action_type),
INDEX idx_status (status),
INDEX idx_executed_at (executed_at)
);
6.2 审计日志服务
package com.unimdp.harness.audit;
@Service
@Slf4j
public class AuditService {
@Autowired
private AuditLogRepository auditLogRepository;
@Autowired
private ObjectMapper objectMapper;
/**
* 记录工具调用审计日志
*/
@Async
public void logToolCall(ToolCallContext context, ToolCallResult result,
List<ConstraintCheck> checks) {
AuditLog log = AuditLog.builder()
.userId(context.getUserId())
.sessionId(context.getSessionId())
.actionType("TOOL_CALL")
.toolName(context.getToolName())
.actionParams(toJson(context.getParams()))
.actionResult(toJson(result))
.decisionPath(toJson(context.getDecisionPath()))
.constraintType(determineConstraintType(checks))
.constraintCheck(toJson(checks))
.status(result.getStatus())
.errorMessage(result.getErrorMessage())
.requestId(context.getRequestId())
.executedAt(Instant.now())
.build();
auditLogRepository.save(log);
}
/**
* 记录约束拦截审计日志
*/
public void logConstraintBlock(ToolCallContext context,
RuleResult ruleResult,
String constraintType) {
AuditLog log = AuditLog.builder()
.userId(context.getUserId())
.sessionId(context.getSessionId())
.actionType("CONSTRAINT_BLOCK")
.toolName(context.getToolName())
.actionParams(toJson(context.getParams()))
.constraintType(constraintType)
.constraintCheck(ruleResult.getMessage())
.status("BLOCKED")
.requestId(context.getRequestId())
.executedAt(Instant.now())
.build();
auditLogRepository.save(log);
log.warn("🔴 CONSTRAINT BLOCK: user={}, tool={}, reason={}",
context.getUserId(), context.getToolName(), ruleResult.getMessage());
}
}
七、配置化约束模板
7.1 约束模板设计
# constraint-templates.yaml
# 约束模板配置——业务人员可通过 YAML 配置,无需发版
templates:
# 工具白名单模板
- id: tool-whitelist
name: 工具白名单
description: 定义 Agent 可以调用的工具列表
type: PERMISSION
parameters:
- name: allowed_tools
type: array
required: true
description: 允许的工具名称列表
expression: |
allowed_tools = params.allowed_tools;
return allowed_tools.contains(toolName);
failAction: BLOCK
# 数据范围模板
- id: data-scope
name: 数据范围限制
description: 限制 Agent 可访问的数据范围
type: PERMISSION
parameters:
- name: allowed_tables
type: array
required: true
- name: max_rows
type: integer
default: 10000
expression: |
allowed_tables = params.allowed_tables;
max_rows = params.max_rows ? params.max_rows : 10000;
return allowed_tables.contains(tableName) && rowCount <= max_rows;
failAction: BLOCK
# 频次限制模板
- id: rate-limit
name: 调用频次限制
description: 限制工具在单位时间内的调用次数
type: PROCESS
parameters:
- name: max_calls
type: integer
required: true
- name: time_window
type: integer
default: 60
expression: |
max_calls = params.max_calls;
time_window = params.time_window ? params.time_window : 60;
calls_in_window = getCallsInWindow(toolName, time_window);
return calls_in_window < max_calls;
failAction: WARN
7.2 约束模板加载器
package com.unimdp.harness.template;
@Component
@Slf4j
public class ConstraintTemplateLoader {
private final Map<String, ConstraintTemplate> templates = new ConcurrentHashMap<>();
@PostConstruct
public void loadTemplates() {
try {
// 从 YAML 文件加载模板
Resource resource = new ClassPathResource("constraint-templates.yaml");
Yaml yaml = new Yaml();
Map<String, Object> config = yaml.load(resource.getInputStream());
List<Map<String, Object>> templateList = (List<Map<String, Object>>) config.get("templates");
for (Map<String, Object> tmpl : templateList) {
ConstraintTemplate template = parseTemplate(tmpl);
templates.put(template.getId(), template);
log.info("Loaded constraint template: {}", template.getName());
}
} catch (Exception e) {
log.error("Failed to load constraint templates", e);
}
}
/**
* 热加载模板(支持运行时更新)
*/
public void reloadTemplates() {
templates.clear();
loadTemplates();
log.info("Constraint templates reloaded");
}
/**
* 根据模板创建约束规则
*/
public ConstraintRule createRule(String templateId, Map<String, Object> params) {
ConstraintTemplate template = templates.get(templateId);
if (template == null) {
throw new IllegalArgumentException("Template not found: " + templateId);
}
return ConstraintRule.builder()
.id(UUID.randomUUID().toString())
.templateId(templateId)
.name(template.getName())
.expression(template.getExpression())
.params(params)
.failAction(template.getFailAction())
.build();
}
}
八、总结
本文系统拆解了 Harness 约束层的工程实现,核心要点如下:
8.1 三层防御体系
| 层级 | 作用 | 实现方式 |
|---|---|---|
| 认知约束 | 在决策前注入行为准则 | System Prompt + AGENTS.md |
| 权限约束 | 在工具调用前校验权限 | 规则引擎 + 拦截器 |
| 流程约束 | 在执行过程中规范流程 | 验证器链 + 状态机 |
8.2 动态规则引擎
使用 Aviator 表达式引擎实现约束规则的动态配置和热加载,业务人员可通过 YAML 模板配置约束规则,无需发版。
8.3 Docker 沙箱隔离
通过 Docker 容器化执行 Agent 的操作,实现文件系统隔离、网络隔离和资源限制。
8.4 完整的审计日志
全量记录每一次工具调用和约束拦截,满足合规审计要求,并为后续的 Harness 优化提供数据支撑。
下一篇预告:我们将深入三大引擎的协同工作流,完整展示从用户消息到智能决策的端到端链路。
敬请期待!🚀
📌 本文收录于专栏:从 OpenClaw 到 Hermes:自改进 Agent 完全指南
179

被折叠的 条评论
为什么被折叠?



