feat: 完善 Agent 标准交互与安全运行时

- 接入 AG-UI 运行投影、Turn 时间线和审批隔离

- 增加 Agent Skill 冻结绑定与运行时消费闭环

- 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
2026-08-19 22:13:41 +08:00
parent 91d66e636d
commit 4e8640dcaf
241 changed files with 24382 additions and 2777 deletions

View File

@@ -39,7 +39,7 @@ import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -136,7 +136,9 @@ public class ApprovalQueryServiceImplAccessTest {
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
ApprovalInstanceDetailVo detail = service.detail(INSTANCE_ID);
assertSame(snapshot, detail.getSnapshotJson());
assertNotSame(snapshot, detail.getSnapshotJson());
assertEquals(Map.of(), detail.getSnapshotJson().get("resourceSnapshot"));
assertTrue(detail.getSnapshotJson().containsKey("steps"));
assertFalse(detail.isCanApprove());
assertFalse(detail.isCanReject());
assertTrue(detail.isCanRevoke());

View File

@@ -0,0 +1,146 @@
package tech.easyflow.approval.support;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
/**
* {@link ApprovalSnapshotProjection} 响应脱敏契约测试。
*/
public class ApprovalSnapshotProjectionTest {
/**
* 验证 Skill 审批仅保留标准包和 Tool 绑定摘要,不返回正文、资源正文或 MCP 连接信息。
*/
@Test
public void shouldProjectSkillSnapshotToSafeSummary() {
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("previousStatus", "DRAFT");
snapshot.put("resourceSnapshot", Map.of(
"schemaVersion", 2,
"name", "contract-review",
"description", "合同审查",
"skillContent", "# private instructions",
"resources", List.of(Map.of(
"path", "references/private.md",
"content", "private reference",
"contentRef", "internal-ref")),
"packageHash", "package-hash",
"contentSnapshotHash", "content-hash",
"platformToolBindings", Map.of(
"bindings", List.of(Map.of(
"toolType", "MCP",
"targetId", 9,
"displayName", "GitHub",
"toolCount", 2,
"mcpToolManifestHash", "manifest-hash",
"resourceSummary", Map.of(
"title", "GitHub",
"baseUrl", "https://internal.example?token=secret",
"requestHeaders", Map.of("Authorization", "secret")),
"mcpToolManifest", List.of(Map.of(
"name", "search",
"inputSchema", Map.of("token", "secret"))),
"resourceSnapshot", Map.of(
"configJson", "{\"env\":{\"TOKEN\":\"secret\"}}",
"headers", Map.of("Authorization", "secret"),
"environment", Map.of("TOKEN", "secret")))),
"snapshotHash", "tools-hash"),
"toolBindingsHash", "tools-hash",
"snapshotHash", "aggregate-hash"));
Map<String, Object> projected = ApprovalSnapshotProjection.project("SKILL", snapshot);
Map<?, ?> resource = (Map<?, ?>) projected.get("resourceSnapshot");
Map<?, ?> platformBindings = (Map<?, ?>) resource.get("platformToolBindings");
Map<?, ?> binding = (Map<?, ?>) ((List<?>) platformBindings.get("bindings")).get(0);
assertEquals("contract-review", resource.get("name"));
assertEquals("合同审查", resource.get("description"));
assertEquals("aggregate-hash", resource.get("snapshotHash"));
assertEquals(1, resource.get("resourceCount"));
assertEquals("manifest-hash", binding.get("mcpToolManifestHash"));
assertEquals(2, binding.get("toolCount"));
assertEquals(1, binding.get("manifestToolCount"));
assertFalse(resource.containsKey("skillContent"));
assertFalse(resource.containsKey("resources"));
assertFalse(binding.containsKey("mcpToolManifest"));
assertFalse(binding.containsKey("resourceSnapshot"));
Map<?, ?> bindingSummary = (Map<?, ?>) binding.get("resourceSummary");
assertEquals("GitHub", bindingSummary.get("title"));
assertFalse(bindingSummary.containsKey("baseUrl"));
assertFalse(bindingSummary.containsKey("requestHeaders"));
assertTrue(serialize(projected).indexOf("secret") < 0);
}
/**
* 验证 Agent 审批保留绑定摘要与 hash同时递归移除配置正文和嵌套 Skill 运行快照。
*/
@Test
public void shouldProjectAgentSnapshotToSafeSummary() {
Map<String, Object> resourceSnapshot = new LinkedHashMap<>();
resourceSnapshot.put("id", 7);
resourceSnapshot.put("name", "审查 Agent");
resourceSnapshot.put("description", "用于审查合同");
resourceSnapshot.put("promptConfigJson", Map.of("systemPrompt", "private prompt"));
resourceSnapshot.put("modelConfigJson", Map.of("apiKey", "secret"));
resourceSnapshot.put("basicSummary", Map.of("name", "审查 Agent", "status", 1));
resourceSnapshot.put("skillBindings", List.of(Map.of(
"skillId", 21,
"sortNo", 0,
"resourceSummary", Map.of(
"displayName", "合同审查",
"snapshotHash", "skill-hash",
"textResourceCount", 3),
"resourceSnapshot", Map.of(
"skillContent", "private skill",
"resources", List.of(Map.of("content", "private reference")),
"source", "easyflow://internal"))));
Map<String, Object> projected = ApprovalSnapshotProjection.project(
"agent", Map.of("resourceSnapshot", resourceSnapshot));
Map<?, ?> resource = (Map<?, ?>) projected.get("resourceSnapshot");
Map<?, ?> binding = (Map<?, ?>) ((List<?>) resource.get("skillBindings")).get(0);
Map<?, ?> summary = (Map<?, ?>) binding.get("resourceSummary");
assertEquals("审查 Agent", resource.get("name"));
assertEquals("合同审查", summary.get("displayName"));
assertEquals("skill-hash", summary.get("snapshotHash"));
assertEquals(3, summary.get("textResourceCount"));
assertFalse(resource.containsKey("promptConfigJson"));
assertFalse(resource.containsKey("modelConfigJson"));
assertFalse(binding.containsKey("resourceSnapshot"));
assertTrue(serialize(projected).indexOf("private skill") < 0);
assertTrue(serialize(projected).indexOf("easyflow://internal") < 0);
}
/**
* 验证历史审批资源保持兼容,但详情响应使用独立深复制 Map。
*/
@Test
public void shouldKeepLegacyResourceSnapshotsCompatible() {
Map<String, Object> snapshot = Map.of(
"resourceSnapshot", Map.of("title", "流程", "content", "workflow source"));
Map<String, Object> projected = ApprovalSnapshotProjection.project("WORKFLOW", snapshot);
assertNotSame(snapshot, projected);
assertEquals(snapshot, projected);
}
/**
* 将测试对象转成稳定字符串,便于断言敏感字面值未出现在任意嵌套层级。
*
* @param value 待检查对象
* @return 对象字符串
*/
private String serialize(Object value) {
return String.valueOf(value);
}
}