From 4ccf7b10625d211d5aa1cc559053e6fd35e9078b Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Wed, 27 May 2026 21:17:32 +0800 Subject: [PATCH] feat: complete swarm manager callback loop --- ...de-manager-sub-swarm-progress-checklist.md | 295 ++++--------- docs/heicode-manager-swarm-gap-analysis.md | 112 +++++ ...nt-manager-swarm-runtime-change-request.md | 403 ++++++++++++++++++ .../heicode-desktop-sub-agile-api.md | 2 +- ...code-manager-swarm-runtime-env-template.md | 151 +++++++ heicode/VERSION | 2 +- heicode/controller/agnet_approval.go | 2 + heicode/controller/agnet_approval_test.go | 66 +++ heicode/controller/agnet_control_plane.go | 195 +++++++++ .../controller/agnet_control_plane_test.go | 59 +++ heicode/controller/agnet_runtime_client.go | 161 ++++++- .../default/src/features/agnet-console/api.ts | 3 + .../src/features/agnet-console/pages.tsx | 108 ++++- 13 files changed, 1336 insertions(+), 223 deletions(-) create mode 100644 docs/heicode-manager-swarm-gap-analysis.md create mode 100644 docs/integration/agent-manager-swarm-runtime-change-request.md create mode 100644 docs/integration/heicode-manager-swarm-runtime-env-template.md diff --git a/docs/heicode-manager-sub-swarm-progress-checklist.md b/docs/heicode-manager-sub-swarm-progress-checklist.md index db004dcd..91f6862f 100644 --- a/docs/heicode-manager-sub-swarm-progress-checklist.md +++ b/docs/heicode-manager-sub-swarm-progress-checklist.md @@ -1,232 +1,125 @@ -# Heicode Manager sub 模式与蜂群模式进度清单 +# Heicode Manager 蜂群模式进度清单 -更新时间:2026-05-26 +更新时间:2026-05-27 负责人范围:Heicode Manager 端 -文档目的:给项目负责人、上级和联调同学快速看到当前项目流程、已完成项、未完成项、原因、依赖和下一步。 +用途:给负责人、上级和联调同学快速确认 Manager 端在蜂群模式下已经具备什么、还要做什么、哪些需要客户端或蜂群项目配合。 ## 资料来源 | 来源 | 用途 | |---|---| -| `http://gitee.ath.cx:3000/taijibaga/heicode/src/branch/main/docs/product-package` | Heicode 产品资料包,定义客户端、Manager、Agnet 平台、模型、密钥、审批和交付边界 | -| `http://gitee.ath.cx:3000/taijibaga/fengqun/src/branch/main/docs` | 蜂群资料包,定义蜂群最小闭环、生产级差距、任务图、claim、handoff、artifact、审批和验收口径 | -| 当前仓库 `heicode/` 代码 | Manager 实际已实现能力核查 | -| 当前仓库 `docs/product-package/` | 本仓库内同步的 Heicode 产品资料包,用于核对远端资料包落地口径 | -| `docs/heicode-runtime-auth-newapi-secret-design.md` | 当前 Manager / Agnet / NewAPI / Azure Key Vault 的运行时边界修正文档 | -| `docs/integration/agnet-platform-request-contract.md` | Manager 到 Agnet 平台的本地最小控制面与生产联调契约 | +| `http://gitee.ath.cx:3000/taijibaga/fengqun/src/branch/main/docs` | 蜂群设计资料包,定义目标驱动蜂群、任务图、claim、heartbeat、handoff、artifact、审批、审计和三方分工 | +| `http://gitee.ath.cx:3000/taijibaga/HeiCode-Swarm` | 蜂群项目实现资料,当前 Orchestrator/Agent/Redis/K8s/桌面演示客户端的实际结构 | +| `docs/product-package/07-integration-boundaries.md` | Heicode、Manager、Agnet 平台、CodeGW、Azure Key Vault 的边界 | +| `docs/integration/heicode-desktop-sub-agile-api.md` | Heicode 桌面客户端接 Manager 的普通 sub 敏捷流程 | +| 当前仓库 `heicode/` 代码 | Manager 端实际实现核查 | -## 二次核查结论 +## 一、核心边界 -本清单已经按 2026-05-26 当前代码和资料包重新核查。下面结论只按代码或文档证据填写,不把规划项写成已完成。 +蜂群模式不是普通 sub 敏捷/瀑布本身。普通 sub 是 Heicode 的任务组织方式;蜂群是 Agnet/Swarm Runtime 的执行方式。 -| 核查项 | 确认结果 | 证据 | -|---|---|---| -| Heicode 产品资料包 | 远端资料包与本仓库 `docs/product-package/` 口径一致,当前用户侧叫“密钥保管器”,技术实现写 Azure Key Vault | `docs/product-package/README.md`、`docs/product-package/01-product-overview.md`、远端 Gitea contents API | -| 蜂群资料包 | `fengqun` 已完成的是 `swarm-minimal` 本地最小闭环,不等于生产级平台联调完成 | 远端 `MINIMAL_CLOSED_LOOP_STATUS.zh-CN.md`、`01-ten-day-delivery-plan.md` | -| 瀑布 / 敏捷与蜂群关系 | Heicode 可用瀑布/敏捷表达任务组织或阶段门;蜂群 Runtime 不能依托瀑布、敏捷、Scrum 固定流程 | `docs/heicode.md`、远端 `agnet-swarm-design-principles.md` | -| Manager Agnet 控制面 | 本仓库实现的是 `/api/agnet/deployments` 本地最小控制面,占位保存 deployment、manifest、audit、logs/metrics 空壳;未真实创建生产 Swarm Run | `heicode/controller/agnet_control_plane.go`、`docs/integration/agnet-platform-request-contract.md` | -| 用户任务入口 | 首页已通过 mcp-server 代理创建 HeicodeTask、追问和任务卡;但任务卡尚未自动转换成本地 Agnet deployment 或真实 Swarm Run | `heicode/web/default/src/lib/heicode-mcp.ts`、`heicode/web/default/src/features/tasks/task-card-view.tsx` | -| 普通用户部署权限 | 当前 `/api/agnet/deployments` 创建/查询/停止走 `AdminAuth`,不满足普通用户在 Manager 辅助控制台发起部署的产品闭环 | `heicode/router/api-router.go` | -| 平台回调 | 当前没有 `POST /api/agnet/callbacks/swarm-events`、`approval-requests`、`artifacts`、`usage`、`status` 这类服务端回调入口 | `heicode/router/api-router.go`、远端 `agnet-swarm-design-principles.md` | -| 审批能力 | Manager 有用户态 approval/lease 记录 API,但这不是 Agnet 平台服务端回调审批主线,也没有回传 Runtime 继续/停止 | `heicode/controller/agnet_approval.go` | - -## 结论摘要 - -1. sub 模式下的瀑布 / 敏捷,和蜂群模式不是一回事。 -2. sub 模式瀑布 / 敏捷属于 Heicode 的任务组织方式和用户体验层,决定任务怎么拆、怎么推进、怎么确认。 -3. 蜂群模式属于 Agnet / Swarm Runtime 执行层,决定多个 Agent / worker 如何动态生成任务图、claim 任务、交接、失败恢复、回传事件和交付物。 -4. Manager 侧已经完成资源、密钥、权限、任务草稿入口、任务卡展示、Agnet 部署本地占位、审批记录、短期凭证租约、审计展示等基础控制面。 -5. 当前还没有完成生产级蜂群闭环:没有真实 `POST /api/swarms` 或生产 Agnet 平台创建联调、没有真实 worker runtime claim、没有 artifact / event callback 完整接收、没有客户端高危审批主体验闭环。 -6. 蜂群资料包明确说明:`fengqun` 当前达到本仓库定义的最小化闭环验收,但不是生产级完整交付;生产级差距在 Kubernetes worker runtime、Manager / Agnet API、人类审批主线、生产权限和监控。 -7. 当前 Manager 端最大风险不是“没有页面”,而是“页面/接口已有一部分,但仍是本地控制面、管理员权限或 mcp-server 任务草稿,尚未和生产 Swarm Runtime 打通”。 - -## 概念边界 - -| 概念 | 所属层 | 当前定位 | 不能误解为 | +| 系统 | 定位 | 应该做什么 | 不应该做什么 | |---|---|---|---| -| sub 瀑布 | Heicode 任务组织层 | 按需求、设计、开发、测试、部署等阶段推进 | 蜂群 Runtime | -| sub 敏捷 | Heicode 任务组织层 | 小步迭代,允许多轮反馈和调整 | 蜂群 Runtime | -| 蜂群模式 | Agnet / Swarm Runtime 层 | 动态任务图、能力编队、任务池 claim、handoff、失败恢复、事件和产物回传 | 瀑布 / 敏捷 / Scrum | -| Manager | 控制面 / 辅助控制台 | 资源绑定、密钥引用、权限、启动摘要、状态、审计、审批记录 | 编码主体验或 Runtime 执行器 | -| 客户端 | 用户主体验 | 输入想法、持续推进任务、查看反馈、接收交付、高危审批 | 资源后台或模型供应商配置页 | -| 蜂群平台 | 执行层 | AKS / worker runtime、任务图、claim、事件、artifact、指标 | 用户控制台 | +| Heicode 桌面客户端 | 用户主体验 | 输入目标、持续补充需求、查看反馈、审批高危操作、接收交付结果 | 直接配置 AKS、模型供应商、完整蜂群 payload | +| Heicode Manager | 控制面和记录面 | 资源绑定、`secret_ref`、权限清单、生成启动请求、记录 deployment/swarm 映射、回调、artifact、timeline、审批、审计 | 替代客户端做主开发对话,或替代 Runtime 执行任务 | +| HeiCode-Swarm / Agnet Runtime | 执行层 | 创建 Swarm Run、任务图、Agent 编队、claim、heartbeat、handoff、执行、结果回传 | 保存长期明文密钥,直接暴露给普通用户 | -## 总体流程 +## 二、目标调用链 ```text -用户输入想法 --> Heicode 形成任务目标、约束和上下文 --> Manager 准备资源绑定、secret_ref、Resource Grant、审批策略和启动摘要 --> sub 模式选择任务组织方式(瀑布 / 敏捷) --> Manager 生成 Agnet / Swarm 请求 payload --> 蜂群平台创建 Swarm Run / 动态任务图 / 能力编队 --> 子 Agnet claim 任务、执行、handoff、失败恢复 --> 高危动作请求客户端审批 --> 审批通过后派生短期凭证 --> 蜂群平台回传状态、事件、日志、指标、artifact、用量、审计 --> Manager 展示状态和审计,客户端展示执行反馈和交付结果 +Heicode 桌面客户端 + -> Heicode Manager + - V2 加密请求 body + - task/deployment draft + - resource_grants / secret_ref / budget / approval_policy + -> Agnet Runtime 或 HeiCode-Swarm + - POST /api/swarms 或兼容创建入口 + - 返回 swarm_id / runtime_deployment_id + <- Runtime callback + - swarm-events / artifact.created / approval.requested / timeline.updated + <- Manager 查询接口 + - deployment detail / events / logs / metrics / artifacts / sk-snapshots / timeline + <- 桌面客户端展示和审批 ``` -## 已完成清单 +## 三、当前 Manager 已完成项 -| 模块 | 已完成内容 | 代码 / 页面证据 | 当前状态 | -|---|---|---|---| -| 登录与用户 | Manager 支持 Heicode 登录、会话、用户上下文、`sub` / user id 作为业务身份基础 | `heicode/controller/heicode_oauth.go`、`heicode/router/api-router.go` | 已完成基础能力 | -| 模型与余额 | 客户端模型列表、余额、用量基础链路已接 Manager / NewAPI | `/api/heicode/self`、模型/渠道页面 | 已完成基础能力 | -| 设备绑定 | 设备配对、撤销、重新配对、审计、真实 IP、设备列表体验已修复 | `devices` 相关接口与页面 | 已完成 | -| 任务想法入口 | 首页可通过 mcp-server 代理提交 idea,生成 HeicodeTask 并进入追问/任务卡 | `heicode/web/default/src/features/dashboard/components/home-hero.tsx`、`heicode/web/default/src/lib/heicode-mcp.ts` | 已完成任务草稿入口,未接 Agnet 部署 | -| 任务追问与任务卡 | 支持 `configuring` 追问、答题、任务卡目标/范围/自动生成/Manager 辅助入口展示 | `heicode/web/default/src/features/tasks/task-card-view.tsx` | 已完成基础体验 | -| 资源绑定 | 支持 Git / SK / 项目文档 / 云账号 / 云资源等 Resource Binding 数据结构 | `heicode/controller/resource.go`、`heicode/model/resource.go` | 已完成基础能力 | -| 资源授权 | 支持 Resource Grant,能生成 permission manifest | `/api/resource-grants`、`/api/resource-grants/manifest` | 已完成基础能力 | -| 资源撤销 | Resource Binding 和 Grant 删除改为 revoked,保留审计链路 | `DeleteResource`、`DeleteResourceGrant` | 已完成 | -| Azure Key Vault | 支持 VM Managed Identity 访问 Key Vault、PutSecret、GetSecret、健康状态检查、`azkv://...` 引用 | `heicode/controller/secret_store.go` | 已完成 Manager 侧 | -| Azure 资源发现 | Azure cloud account 绑定后可读取 Key Vault 中 SP 凭证并调用 ARM 发现资源 | `heicode/controller/azure_resource_discovery.go` | 已完成 Azure 第一阶段 | -| Agnet 角色 | 提供六角色模板:Product / Architect / Frontend / Backend / Reviewer / Ops | `heicode/controller/agnet_role_template.go` | 已完成 | -| Agnet 部署占位 | 支持创建、查询、停止 Agnet deployment,占位状态持久化到 DB;能校验 budget、runtime、resource_grants、secret_ref | `heicode/controller/agnet_control_plane.go`、`heicode/model/agnet_deployment.go` | 已完成控制面占位,非真实 Runtime | -| 权限清单展示 | 部署详情优先展示持久化 permission manifest,密钥引用脱敏 | `heicode/web/default/src/features/agnet-console/pages.tsx` | 已完成 | -| 审计事件 | Agnet audit event 持久化,不再仅内存保存;deployment events/logs 可从审计表回放 | `heicode/model/agnet_audit.go`、`AgnetListDeploymentEvents`、`AgnetListDeploymentLogs` | 已完成本地审计 | -| SK 快照解析 | 可以根据 deployment 中的 `sk_sources` 生成 SK snapshot 摘要 | `AgnetResolveSKSnapshots`、`AgnetListSKSnapshots` | 已完成内存级占位,未持久化 | -| 审批记录 | 支持 Agnet approval request 创建、审批、拒绝、过期 | `heicode/controller/agnet_approval.go` | 已完成 Manager 记录层 | -| 短期凭证租约 | 审批通过后可生成 `lease://agnet/...`,支持撤销和过期 | `heicode/model/agnet_approval.go` | 已完成 Manager 记录层 | -| 审计页面 | 可查看审批、lease、审计流水,敏感字段脱敏 | `/audit`、`AgnetAuditPage` | 已完成基础展示 | -| 密钥状态页面 | 系统设置中可看 Key Vault 配置、认证方式、可达状态 | `/api/secret-store/status` | 已完成 | -| 后台菜单 | 模型、模型部署、供应商、全部使用日志等菜单错绑已修复 | 后台菜单点击冒烟通过 | 已完成 | -| 版本与上线 | Manager 生产版本为 1.4.4,线上 VM 构建部署已完成 | `https://code.xinghanlab.com/api/status` | 已上线 | +以下只按当前仓库代码确认,不把规划项写成已完成。 -## 未完成清单 +| 能力 | 当前状态 | 代码证据 | +|---|---|---| +| sub 模式字段 | 已支持 `sub_mode`,默认 `agile`,校验 `agile/waterfall` | `heicode/controller/agnet_control_plane.go`、`heicode/model/agnet_deployment.go` | +| 用户态 deployment | 已有 `/api/agnet/user/deployments` 创建、查询、停止、日志、事件、指标、artifact、SK snapshot、timeline | `heicode/router/api-router.go` | +| 任务到 deployment draft | 已有 `/api/agnet/user/tasks/:task_id/deployment-draft` | `heicode/controller/agnet_task_bridge.go` | +| `/api/swarms` 兼容入口 | 已有用户态 `POST /api/swarms`,内部走 Manager deployment 创建,并作为 adapter source 记录 | `heicode/router/api-router.go`、`AgnetCreateUserSwarm` | +| Runtime 创建桥接 | 已能按配置调用 Runtime 创建接口,默认路径 `/api/agnet/deployments`,可用环境变量改为蜂群创建路径 | `heicode/controller/agnet_runtime_client.go` | +| Runtime stop 桥接 | 已能在停止 Manager deployment 时调用 Runtime stop | `heicode/controller/agnet_runtime_client.go` | +| callback 接收 | 已有 `POST /api/agnet/callbacks/swarm-events` | `heicode/controller/agnet_callback.go` | +| callback 鉴权 | 支持 `X-Agnet-Service-Token` 和 HMAC 签名校验,并可从 Key Vault ref 读取签名密钥 | `heicode/controller/agnet_callback.go` | +| callback 幂等 | `event_id` / `idempotency_key` 去重,重复回调返回成功但不重复写 | `heicode/model/agnet_callback.go` | +| artifact 落库 | `artifact.created` 可生成 artifact 记录,支持用户态列表查询 | `heicode/model/agnet_artifact.go`、`AgnetListUserDeploymentArtifacts` | +| approval 回调 | `approval.requested` 可转成 Manager 审批记录 | `heicode/controller/agnet_callback.go` | +| 审批结果回传 Runtime | 用户 approve/reject 后,Manager 可按配置 POST 回 Runtime approval decision,且不发送 `secret_ref` | `heicode/controller/agnet_approval.go`、`heicode/controller/agnet_runtime_client.go` | +| timeline 聚合 | 用户态 timeline 聚合 audit、callbacks、artifacts、sk_snapshots | `AgnetGetUserDeploymentTimeline` | +| SK snapshot 持久化 | 已有 `agnet_sk_snapshots` 模型和列表查询 | `heicode/model/agnet_sk_snapshot.go` | +| 本地模拟事件 | 已有用户态 `simulate-events`;默认模拟会写入 callback、artifact、approval、timeline 记录,用于 Manager 自测展示链路和脱敏检查 | `AgnetSimulateUserDeploymentEvents` | +| V2 body 加密 | `/api/agnet/user/*` 和 `/api/heicode-auth/*` 已支持桌面端 V2 加密 body | `heicode/middleware/auth.go` | +| 生产普通 sub 烟测 | 已验证 production create/detail/metrics/events/logs/artifacts/sk-snapshots/timeline/stop 链路 | `docs/integration/heicode-desktop-sub-agile-api.md` | -| 模块 | 未完成内容 | 没做完的原因 | 缺什么 | 归属 | +## 四、Manager 端还需要继续做的蜂群任务 + +| 优先级 | 任务 | 当前缺口 | 是否 Manager 可独立做 | 验收标准 | |---|---|---|---|---| -| sub 模式字段 | 没有完整的 `sub_mode = waterfall/agile` 任务字段、接口参数、页面选择和后端持久化 | 产品允许用瀑布/敏捷表达任务组织,但蜂群 Runtime 不应绑定固定方法论;当前代码没有统一字段 | 任务模型字段、选择入口、payload 映射、验收状态 | Manager 可先做 | -| sub 任务编排 | HeicodeTask 没有自动生成 Agnet deployment payload 并启动 | mcp-server 任务卡和本地 `/api/agnet/deployments` 是两条线,尚未打通 | task -> recommendation -> resource grants -> deployment payload 的桥接层 | Manager 可先做,客户端配合验收 | -| 用户态部署权限 | 普通用户无法按产品闭环直接创建 Agnet deployment | 后端 deployment 控制面当前使用 `AdminAuth`,更像联调/管理接口 | 设计 user-scoped create/query/stop,校验 resource grants 和用户归属 | Manager 可先做 | -| API 命名统一 | 蜂群资料包使用 `POST /api/swarms`,Manager 本地契约和代码使用 `/api/agnet/deployments` | 两份资料来源处于不同层:蜂群目标平台 vs Manager 当前最小控制面 | 明确 adapter 或统一接口命名,避免联调时双方对不上 | Manager + 蜂群平台 | -| 瀑布 / 敏捷流程展示 | 客户端/Manager 还不能按瀑布或敏捷展示子阶段状态 | 缺少统一任务阶段模型和状态事件 | `stage`, `iteration`, `checkpoint`, `approval_gate` 等字段 | Manager + 客户端 | -| 真实蜂群创建 | Manager 未真实调用蜂群平台 `POST /api/swarms` 或等价接口 | 当前 `/api/agnet/deployments` 是 Manager 本地控制面占位 | 蜂群平台稳定 API、认证、请求/响应契约、联调环境 | 需要蜂群平台 | -| `swarm_id` 绑定 | Manager 未保存真实 `swarm_id`、task graph、worker 状态 | 真实 Runtime 尚未联调 | Swarm Run 模型、状态表、映射关系 | Manager + 蜂群平台 | -| 动态任务图 | 没有展示真实动态任务图、任务 claim、heartbeat、release、timeout | Manager 没有收到 Runtime 任务事件 | `swarm_tasks` / `task_graph` / `claim` / `heartbeat` 事件 | 需要蜂群平台 | -| handoff / retry | 没有真实失败、交接、重试、blocked 状态闭环 | 目前事件主要来自 Manager 审计,占位性质 | Runtime 失败注入、handoff 事件、重试策略 | 需要蜂群平台 | -| artifact 回传 | 没有 artifact 接收表、回调接口和交付物页面闭环 | 当前没有蜂群平台 artifact callback | artifact schema、存储位置、回调签名、展示页面 | Manager 可建接口,需蜂群输出 | -| 真实日志流 | 没有生产级实时日志流,当前 logs 主要是脱敏审计占位 | 蜂群平台日志接口/SSE 未接入 | `/logs/stream` 或事件流、脱敏规则、保留策略 | 需要蜂群平台 | -| 真实指标 | Metrics 仍是空/估算,不是 Runtime CPU、内存、耗时、token、成本 | 缺少 Runtime 指标源 | Prometheus/监控接口、指标字段、correlation_id | 需要蜂群平台/基础设施 | -| callback 接收 | Manager 缺少统一 `swarm-events`、`artifacts`、`approval-requests`、`usage`、`status` callback 接收端 | 目前是 Manager 拉取/本地记录,不是平台服务端回调;approval API 也需要用户认证,不适合 Runtime 直接调用 | 回调 URL、签名验证、幂等 key、重复回调处理、服务身份认证 | Manager 可先做,需蜂群调用 | -| 客户端审批主体验 | 高危审批现在是 Manager API/后台页面能力,不是客户端主审批弹窗 | 产品文档要求客户端审批,Manager 不替代客户端 | 客户端审批 UI、push/轮询、approval_id 回传 | 需要客户端 | -| 审批回传蜂群 | 审批通过/拒绝后未真实通知 Runtime 继续或停止 | 蜂群平台审批等待状态机未联调 | approval result callback/API | Manager + 蜂群平台 | -| 短期凭证注入 | Manager 只有 `lease://agnet/...` 记录,不负责真实派生/注入子 Agnet runtime | 真实凭证派生与注入应在受控 Runtime/平台完成 | Key Vault 读取策略、最小权限临时凭证、runtime 注入通道 | 需要蜂群平台/基础设施 | -| SK 快照持久化 | SK snapshot 当前存内存,容器重启会丢 | `agnetSnapshots` 是内存 map,没有对应 DB 表 | snapshot 表、artifact/source 版本、查询分页 | Manager 可先做 | -| SK 工具调用展示 | 没有真实“调用哪个 SK、产出什么、失败原因”的任务视角展示 | 当前只有 SK source / snapshot 占位 | SK invocation event、tool output summary、失败事件 | 需要蜂群/SK 平台 | -| 子 Agnet 用量归属 | 模型用量没有按 task / deployment / role / sub agent 聚合展示 | NewAPI 用量和蜂群任务上下文尚未打通 | request metadata、correlation_id、role、task_id | Manager + NewAPI + 蜂群 | -| 交付结果回流 | 客户端/Manager 没有完整交付物、测试结果、部署链接展示 | Runtime 未产出正式 artifact/result schema | delivery result schema、artifact 存储、客户端展示 | Manager + 客户端 + 蜂群 | -| AWS/GCP | 云资源绑定目前主要做 Azure,AWS/GCP 未实现 | 当前优先级先做 Azure Key Vault / ARM | AWS/GCP 授权、发现、secret_ref、权限映射 | Manager 后续 | -| 历史文档口径 | `docs/heicode.md` 仍保留 Vault/Secret Provider 历史选项,新产品资料包和运行时设计已锁定 Azure Key Vault | 新旧文档有历史分层,容易被误读为还要接 OpenBao/Vault | 保留用户侧“密钥保管器”,实现侧统一写 Azure Key Vault;清理旧优先级表述 | Manager/产品文档可改 | +| P0 | 把 `/api/swarms` adapter 文档化并固定字段 | 代码已有入口,但文档要明确它和 Runtime `POST /api/swarms` / `/api/agnet/deployments` 的映射 | 是 | 文档写清 request/response、source、`deployment_id <-> swarm_id` 映射 | +| P0 | 增加 Swarm Run 显示字段 | 页面需要明确展示 `runtime_deployment_id`、`runtime_swarm_id`、`runtime_state`、source | 是 | 用户态 deployment 详情和后台页面能看到 Runtime 映射 | +| P0 | 回调事件类型收敛 | 目前统一 `swarm-events` 已能接,但需要按蜂群事件标准列出必填字段和展示规则 | 是 | `task.created/claimed/running/completed/failed/handoff/approval/artifact` 都有 schema | +| P0 | Runtime 联调配置模板 | 需要把 `AGNET_RUNTIME_*` 配置、token、callback URL、签名方式整理给蜂群侧 | 是 | 蜂群项目按模板能调用 Manager callback | +| P1 | 审批结果回传 Runtime 联调 | Manager adapter 已有;仍需要 Runtime 提供接收接口并验证状态继续/停止 | 需要 Runtime 接口 | 审批通过/拒绝后 Runtime 状态能继续或停止 | +| P1 | Artifact 展示优化 | 已能在页面展示 artifact 类型和 URI;仍需等 Runtime 输出真实 code_patch/document/test_report/deployment_manifest | 需要 Runtime 数据 | artifact 页面/详情能按类型展示摘要和链接 | +| P1 | 任务图/Agent 状态展示占位 | Manager 还没有真实 task graph/claim/heartbeat 页面结构 | Manager 可先做展示结构,真实数据需 Runtime | 有空态和字段,不宣称真实已运行 | +| P1 | 日志/指标真实来源标识 | deployment 详情、timeline 已强化 runtime/simulated 来源;logs/metrics 仍需 Runtime 真实数据 | 需要 Runtime 数据 | 页面和 API 响应能区分来源 | +| P1 | 高危审批客户端联动文档 | Manager 有 API,桌面客户端需要知道如何轮询/展示/提交审批 | 是 | 客户端文档补齐 approval flow | +| P2 | 蜂群模式验收脚本 | Manager 已有默认模拟链路和定向单测;还需要整理生产 curl 脚本 | 是 | 本地/生产能跑出 callback、artifact、approval、timeline 可见 | -## 按归属拆分 +## 五、需要蜂群项目配合的事项 -### Manager 可以独立继续做 - -| 事项 | 说明 | 优先级 | +| 事项 | 为什么 Manager 不能单独完成 | 蜂群侧需要提供 | |---|---|---| -| 增加 sub 模式字段 | `waterfall` / `agile` 存到任务或部署计划中 | P0 | -| 打通 idea task 到 Agnet deployment | 从任务生成推荐角色、资源、预算、风险、payload | P0 | -| 增加用户态 Agnet deployment API | 在 `UserAuth` 下允许用户创建/查询/停止自己资源范围内的 deployment | P0 | -| callback 接收端骨架 | 先实现事件、artifact、approval-request 回调接口和幂等保存 | P0 | -| artifact 数据模型 | 先建 artifact 表、列表 API、详情展示 | P0 | -| 统一 `/api/swarms` 与 `/api/agnet/deployments` 边界 | 明确 Manager 对外给 Runtime 的生产接口,保留本地 control-plane 兼容层 | P0 | -| 持久化 SK snapshot | 去掉仅内存保存,保证重启后审计和任务上下文不丢 | P1 | -| 任务视角审计聚合 | 按 task/deployment/correlation_id 聚合日志、审批、资源、用量 | P1 | -| 文档口径同步 Azure Key Vault | 清理旧 Vault / Secret Provider 优先级表述,避免被误读为还要接 OpenBao/Vault | P1 | -| AWS/GCP 占位与提示 | 保持 UI 明确“即将支持”,避免误导 | P2 | +| 真实 Swarm Run | Manager 只能发起请求和记录,不能替 Runtime 创建任务图 | 生产 `POST /api/swarms` 或确认使用现有 `/tasks` 兼容方式 | +| 真实 task graph | 任务拆解、依赖、状态机在 Runtime 内部产生 | `swarm_tasks`、依赖关系、状态枚举 | +| claim / heartbeat / release | 这是 worker runtime 行为 | 事件回调或查询接口 | +| handoff / retry / blocked | 任务交接和失败恢复属于 Runtime | 标准事件、重试次数、失败原因、下一步动作 | +| Agent 执行结果 | Manager 不能生成真实代码产物 | artifact schema、Git branch/commit、测试报告、部署结果 | +| Runtime 指标 | CPU、内存、耗时、Agent 存活、任务耗时来自集群 | metrics 查询或 Prometheus 指标映射 | +| 审批等待状态机 | Runtime 要能暂停高危动作并等待 Manager/客户端审批 | approval request 和 approval decision API | -### 需要客户端配合 +## 六、需要桌面客户端配合的事项 -| 事项 | Manager 需要提供 | 客户端需要提供 | +| 事项 | Manager 已有基础 | 客户端需要做 | |---|---|---| -| 高危审批主体验 | approval list/detail/approve/reject API、风险摘要、TTL、resource scope | 审批弹窗、用户确认、拒绝、结果回传 | -| 任务驾驶舱 | task status、stage、event、artifact、delivery API | 持续任务对话、状态展示、交付接收 | -| sub 瀑布/敏捷展示 | stage/iteration/checkpoint 状态数据 | 按模式展示进度和下一步 | +| V2 加密请求 | Manager 已支持 | sub/蜂群相关 POST 请求复用模型调用加密 | +| 任务创建和追问 | Manager 有 `/api/heicode-auth/*` 代理 | 带 Heicode access token 调用任务接口 | +| deployment draft | Manager 有用户态接口 | 从任务卡调用 draft,再创建 deployment/swarm | +| 进度展示 | Manager 有 detail/events/timeline/artifacts 接口 | 做用户主体验展示,不暴露底层 payload | +| 高危审批 | Manager 有 approval API 和回调转审批记录 | 弹窗展示风险、资源、TTL,并提交 approve/reject | -### 需要蜂群 / Agnet Runtime 配合 +## 七、当前不应误报为完成的项 -| 事项 | Manager 需要 | 蜂群平台需要 | -|---|---|---| -| 真实创建 Swarm Run | 创建请求、secret_ref、Resource Grant、预算、审批策略 | `swarm_id`、初始任务图、能力编队、状态返回 | -| 任务 claim / heartbeat | 状态展示、事件保存 | worker claim、heartbeat、release、timeout | -| handoff / retry / blocked | 展示失败和后续动作 | 失败恢复、任务交接、重试上限 | -| event / artifact callback | 回调接收端和幂等处理 | 事件、产物、日志摘要回调 | -| 真实日志 / 指标 | 查询和展示 | 日志流、指标源、脱敏和保留 | -| 短期凭证注入 | 审批记录、lease 引用、secret_ref | 读取/派生短期凭证并注入子 Agnet | - -### 需要基础设施配合 - -| 事项 | 依赖 | +| 项 | 当前真实状态 | |---|---| -| Kubernetes worker runtime | AKS/集群、worker 镜像、服务账号、网络策略 | -| 监控指标 | Prometheus 或等价指标源、日志采集、告警规则 | -| Key Vault 网络安全 | Private Endpoint / 防火墙 / Managed Identity / Workload Identity | -| 生产级回调安全 | 内网访问、签名验证、重放保护、request id / correlation id | +| 蜂群生产闭环 | 未完成。Manager 有控制面和回调骨架,但真实 Runtime 任务图/Agent 执行仍需蜂群项目联调 | +| HeiCode-Swarm 项目等于正式 Heicode 桌面客户端 | 不是。它有自己的 `desktop-client` 演示端,正式链路应走 Heicode 桌面客户端 -> Manager -> Runtime | +| `/api/swarms` 已等于真实 Runtime Swarm Run | 不是。Manager 侧已有 adapter 入口,但是否真实创建 Swarm Run 取决于 Runtime 配置和蜂群接口 | +| artifact/timeline 有接口就等于有真实产物 | 不是。Manager 能接和展示,真实产物必须由 Runtime 回调 | +| 高危审批在 Manager 里点完就闭环 | 不是。产品要求桌面客户端主审批,并且 Runtime 要收到 decision | -## 当前风险 +## 八、后续执行顺序 -| 风险 | 影响 | 应对 | -|---|---|---| -| 把蜂群最小闭环误报为生产级完成 | 上级预期错误,联调时暴露落差 | 文档明确“最小闭环已验收,不等于生产级完整交付” | -| 把瀑布/敏捷和蜂群模式混为一谈 | 产品和技术边界混乱 | sub 模式只表达任务组织,蜂群只表达执行层 | -| Manager 继续堆本地占位接口 | 看起来页面有了,但真实 Runtime 没跑 | 所有占位接口在文档和页面中标清“control-plane placeholder” | -| 客户端审批未接入 | 高危操作不符合产品边界 | Manager 只保留记录/API,最终审批必须走客户端 | -| 历史 Vault / Azure Key Vault 口径混用 | 部署和安全沟通混乱 | 统一用户侧叫“密钥保管器”,当前实现侧写 Azure Key Vault | -| 日志/指标无真实来源 | 无法验收蜂群运行质量 | 等 Runtime 接入后按 correlation_id 验证事件、日志、指标、artifact | - -## 下一步建议 - -### 5/27-5/28:全链路联调与问题收敛 - -| 任务 | 目标 | 验收标准 | -|---|---|---| -| sub 模式字段落地 | 明确任务采用瀑布或敏捷组织方式 | payload / DB / UI 至少有一个可追溯字段 | -| idea -> deployment 桥接 | 从用户想法生成 Agnet deployment 请求 | 不再只手动建部署;任务能关联 deployment_id | -| 用户态部署权限 | 普通用户可在自己资源范围内创建/查看 deployment | 非管理员用户不 403,且不能越权看别人 deployment | -| callback 骨架 | Manager 能接事件和 artifact 回调 | 本地模拟回调可保存、去重、展示 | -| 客户端审批接口联调 | 客户端能读 pending approval 并回传决定 | approval approve/reject 后状态正确 | - -### 5/29-5/30:全面测试、验收与上线风险确认 - -| 任务 | 目标 | 验收标准 | -|---|---|---| -| 真实蜂群接口联调 | Manager 调用蜂群平台创建 Swarm Run | 获得真实 `swarm_id`,状态可查 | -| 任务 claim 可见 | Runtime 任务领取状态回到 Manager | 能看到 created -> claimed -> running | -| artifact / event 可见 | 执行结果和事件回到 Manager | 至少一个 task.completed 和 artifact 摘要 | -| 安全验收 | 密钥不明文,审批有记录,lease 可撤销 | 日志、页面、API 响应均无明文密钥 | -| 最终风险报告 | 明确上线可放行与阻塞项 | 剩余问题按 Manager / 客户端 / 蜂群 / 基础设施分类 | - -## 验收清单 - -| 验收项 | 当前状态 | 通过标准 | -|---|---|---| -| Manager 能保存资源和 `secret_ref` | 已完成 | 资源响应不含明文密钥 | -| Manager 能生成 Resource Grant manifest | 已完成 | manifest 只含授权范围和 `secret_ref` | -| Manager 能创建 Agnet deployment | 已完成占位 | 生产级需真实调用蜂群平台 | -| 普通用户创建 deployment | 未完成 | `UserAuth` 用户能基于自己的 Resource Grant 创建,不要求管理员权限 | -| Manager 能展示审计事件 | 已完成基础 | 生产级需包含 Runtime 事件 | -| Manager 能记录审批和 lease | 已完成记录层 | 生产级需客户端审批和 Runtime 继续/停止 | -| sub 瀑布/敏捷模式 | 未完成 | 有模式字段、状态流和页面展示 | -| `/api/swarms` / `/api/agnet/deployments` 口径 | 未完成 | Manager 与蜂群平台统一创建接口或有明确 adapter | -| 真实 Swarm Run | 未完成 | Manager 获得真实 `swarm_id` | -| 任务 claim | 未完成 | 至少一个任务从 created 到 claimed/running | -| handoff / retry | 未完成 | 失败任务可回流或转交,并可观察 | -| artifact 回传 | 未完成 | Manager/客户端可看到交付物摘要 | -| 日志/指标 | 未完成 | 可按 correlation_id 查询日志、指标、用量 | -| 客户端高危审批 | 未完成 | 客户端审批后 Manager 和 Runtime 状态一致 | - -## 代码索引 - -| 能力 | 主要文件 | -|---|---| -| Agnet 控制面占位 | `heicode/controller/agnet_control_plane.go` | -| Agnet 审批和 lease | `heicode/controller/agnet_approval.go`、`heicode/model/agnet_approval.go` | -| Agnet 审计 | `heicode/model/agnet_audit.go` | -| 资源绑定和授权 | `heicode/controller/resource.go`、`heicode/model/resource.go` | -| Azure Key Vault | `heicode/controller/secret_store.go` | -| Azure 资源发现 | `heicode/controller/azure_resource_discovery.go` | -| 前端 Agnet 控制台 | `heicode/web/default/src/features/agnet-console/` | -| 任务想法入口 | `heicode/web/default/src/features/dashboard/components/home-hero.tsx` | -| Heicode task / mcp client | `heicode/web/default/src/lib/heicode-mcp.ts` | +| 顺序 | 任务 | 负责人范围 | 备注 | +|---:|---|---|---| +| 1 | 固定 Manager -> Swarm adapter 契约 | Manager | 先把 `/api/swarms`、Runtime create path、callback 字段写死成可联调文档 | +| 2 | 跑一次本地模拟 Runtime callback | Manager | 已有默认模拟链路;继续用于验证 callback/artifact/timeline/approval 去重和脱敏 | +| 3 | 给蜂群项目配置 callback URL 和 service token | Manager + 蜂群 | 不传明文长期密钥 | +| 4 | 用 HeiCode-Swarm 当前 Orchestrator 做兼容测试 | Manager + 蜂群 | 先判断是否走 `/tasks` 适配,还是蜂群侧补 `/api/swarms` | +| 5 | 桌面客户端按文档跑任务 -> draft -> create -> timeline -> approval | 客户端 + Manager | 使用 V2 加密 POST | +| 6 | 补页面来源标识和任务图空态 | Manager | 防止把 simulated/control-plane 误认为 runtime | diff --git a/docs/heicode-manager-swarm-gap-analysis.md b/docs/heicode-manager-swarm-gap-analysis.md new file mode 100644 index 00000000..bf51e9b1 --- /dev/null +++ b/docs/heicode-manager-swarm-gap-analysis.md @@ -0,0 +1,112 @@ +# Heicode Manager 蜂群模式缺失对照文档 + +更新时间:2026-05-27 +对照范围:`fengqun` 设计资料、`HeiCode-Swarm` 项目现状、Heicode 产品资料包、当前 Heicode Manager 代码。 + +## 一、对照结论 + +Manager 端已经具备蜂群联调需要的控制面基础:用户态 deployment、task draft、`/api/swarms` adapter、Runtime 创建/停止桥接、callback 接收、artifact、approval、approval decision 回传 adapter、timeline、SK snapshot、V2 加密请求。本地 `simulate-events` 默认链路也能写入 callback、artifact、approval、timeline 记录,便于 Manager 自测页面展示和脱敏。 + +仍然缺的是“真实蜂群 Runtime 产生的数据和状态机”。也就是说,Manager 不是完全没写;缺口主要集中在 Runtime 真正创建 Swarm Run、任务图、Agent 执行、handoff、产物和审批继续/停止闭环。 + +## 二、按文档要求逐项对照 + +| 文档要求 | 当前 Manager 状态 | 是否满足 | 缺失原因 | 需要谁 | +|---|---|---|---|---| +| Heicode 客户端是主体验 | Manager 文档和接口已按客户端调用设计,V2 加密 POST 已支持 | 部分满足 | 桌面客户端还需按文档接任务、展示进度和审批 | 客户端 | +| Manager 是受控入口,不暴露完整 payload 给普通用户 | 用户态 draft/create 已有,可由任务卡生成 plan | 部分满足 | 页面还需要更弱化底层 payload,突出启动摘要和状态 | Manager | +| Manager 调用 `POST /api/swarms` 创建 Swarm Run | Manager 已有 `/api/swarms` adapter,Runtime create path 可配置 | 部分满足 | 是否真实创建 Swarm Run 取决于蜂群 Runtime 是否提供生产接口 | 蜂群 + Manager | +| 保存 `swarm_id`、状态、请求摘要、`correlation_id` | 模型已有 `runtime_swarm_id`、`runtime_state`、payload JSON、correlation metadata | 基本满足 | 真实 `swarm_id` 需要 Runtime 返回 | 蜂群 | +| 接收 `swarm-events` 回调 | 已有 `POST /api/agnet/callbacks/swarm-events` | 满足接收能力 | 还缺真实 Runtime 持续回调 | 蜂群 | +| 重复回调幂等 | 已按 `event_id` / `idempotency_key` 去重 | 满足 | 需要蜂群侧稳定传唯一事件 ID | 蜂群 | +| 接收 artifact 回调 | `artifact.created` 可落库并查询 | 满足接收能力 | 真实 artifact schema 和文件/分支引用需 Runtime 输出 | 蜂群 | +| 展示 Swarm 状态、事件、产物 | deployment detail/events/artifacts/timeline 已有,页面已强化 runtime/simulated 来源和 artifact 类型/URI | 基本满足 | 真实展示内容仍依赖 Runtime 回调真实数据 | 蜂群 | +| 展示 task graph、claim、heartbeat | 当前没有真实 task graph 表和状态展示 | 不满足 | 需要 Runtime 产出 task graph/claim/heartbeat 事件 | 蜂群 + Manager | +| 展示 handoff、blocked、retry | callback 可接任意 event,但没有专门展示和字段约束 | 部分满足 | 需要标准事件 schema 和 Runtime 实际事件 | 蜂群 + Manager | +| 高危审批请求进入 Manager | `approval.requested` callback 可转审批记录;用户 approve/reject 后可按配置 POST 回 Runtime | 部分满足 | 还需要蜂群 Runtime 提供并验证 approval decision 接收接口 | Manager + 蜂群 | +| 高危审批在客户端主体验完成 | Manager 有审批 API | 部分满足 | 桌面客户端要弹窗、轮询/订阅、提交决定 | 客户端 | +| 短期凭证和长期密钥隔离 | Manager 使用 `secret_ref` / `lease://` 记录,不传明文 | 基本满足 | Runtime 侧短期凭证派生/注入未验证 | 蜂群 + 基础设施 | +| CodeGW 用量归属 | Manager 有模型/余额基础,Runtime payload 带 billing_context | 部分满足 | 子 Agent 调用用量按 task/deployment/role 回流未验证 | 蜂群 + CodeGW + Manager | +| 日志和指标 | Manager 有 logs/metrics 接口占位和 runtime state | 部分满足 | CPU/内存/Agent 存活/任务耗时等真实指标来自 Runtime/AKS | 蜂群 + 基础设施 | +| 最终交付回流 | artifact/timeline 接收能力已有 | 部分满足 | 最终交付结果、Git branch/commit、部署 URL 需要 Runtime 输出,客户端展示 | 蜂群 + 客户端 | + +## 三、HeiCode-Swarm 项目现状对 Manager 的影响 + +从 `HeiCode-Swarm` 项目 README 和代码看,当前项目结构是: + +```text +desktop-client -> Orchestrator(FastAPI) -> Redis -> Agent Pods +``` + +当前 Orchestrator 主要入口: + +| 入口 | 当前作用 | 与 Manager 目标契约差异 | +|---|---|---| +| `GET /health` | 健康检查 | 可直接用于 Runtime health | +| `POST /tasks` | 创建任务 | 不是文档要求的 `POST /api/swarms`,字段也不是 Heicode task/resource/secret/budget 结构 | +| `GET /tasks` / `GET /tasks/{id}` | 查询任务 | 可作为早期状态查询,但缺 `swarm_id` 维度 | +| `GET /agents` | Agent 列表 | 可映射到 Agent 状态 | +| `GET /handoffs` | handoff 历史 | 可映射到 Manager timeline | +| `GET /metrics` | Prometheus 指标 | 可映射到 Manager metrics | +| `WS /ws/{agent_id}` | Agent 注册、心跳、任务、结果 | Manager 不应直接接 Agent WS,应该由 Runtime 汇总后回调 Manager | + +因此,Manager 后续联调有两种路线: + +| 路线 | 说明 | 风险 | +|---|---|---| +| 蜂群侧补正式 `POST /api/swarms` | 最符合设计文档,Manager adapter 直接对接 | 需要蜂群项目改接口 | +| Manager 临时适配 `POST /tasks` | 可以先跑通现有 Orchestrator | 字段语义不足,无法完整覆盖 resource_grants、secret_ref、approval_policy、budget、artifact callback | + +建议:生产目标仍以 `POST /api/swarms` 为准;短期可以做 `/tasks` 兼容测试,但必须标记为兼容桥接,不作为最终契约。 + +## 四、缺失项清单 + +### P0:影响蜂群主流程 + +| 缺失项 | 当前状态 | 处理建议 | +|---|---|---| +| 真实 `POST /api/swarms` 联调 | Manager 有 adapter,蜂群当前可见接口是 `/tasks` | 蜂群侧确认是否补 `/api/swarms`;Manager 固定 adapter 文档 | +| `deployment_id <-> swarm_id` 真实映射 | Manager 字段已准备,真实值需 Runtime 返回 | Runtime create response 必须返回 `swarm_id` | +| task graph / claim / heartbeat 事件 | Manager 可接 callback,但没有真实数据 | 蜂群侧定义并回调 `task.created/claimed/heartbeat/released/completed/failed` | +| artifact 真实产出 | Manager 能落库,缺 Runtime 产出 | 蜂群侧回调 `artifact.created`,带 Git branch/commit 或存储 URI | +| 审批结果回传 Runtime | Manager adapter 已完成,真实闭环未验证 | 蜂群侧提供接收接口并验证 approved/rejected 后继续或停止 | + +### P1:影响可观测和验收 + +| 缺失项 | 当前状态 | 处理建议 | +|---|---|---| +| handoff/retry/blocked 展示 | 可接通用 callback,但页面未专门展示 | 先定事件 schema,再补 timeline 展示 | +| Runtime 日志/指标 | Manager 有 logs/metrics 位置,缺真实数据源 | 蜂群侧提供日志摘要或查询接口;指标对齐 Prometheus | +| 事件来源标识 | API 有部分 runtime/simulated 状态,页面还需强化 | Manager 页面区分 `manager`、`runtime`、`simulated` | +| 桌面客户端审批主流程 | Manager 有 API,客户端未完成主体验 | 客户端按 Manager approval API 接入 | +| 子 Agent 用量归属 | billing_context 有,真实用量未回流 | Runtime 调模型时带 task/deployment/role correlation | + +### P2:完善项 + +| 缺失项 | 当前状态 | 处理建议 | +|---|---|---| +| 蜂群验收脚本 | Manager 默认模拟和单测已覆盖 callback/artifact/approval/timeline;生产 curl 脚本仍需整理 | 增加一套可给联调方直接执行的 curl 脚本 | +| 页面术语统一 | sub/蜂群容易混淆 | 页面和文档统一:sub 是任务组织,swarm 是执行层 | +| 兼容 `HeiCode-Swarm` demo client 的说明 | 容易误认为正式 Heicode 桌面客户端 | 文档明确 demo client 不等于 `cc-haha` 正式客户端 | + +## 五、验收口径 + +蜂群模式不能只看 Manager 页面有没有数据。必须同时满足: + +1. 桌面客户端能通过 Manager 发起任务,POST 请求体走 V2 加密。 +2. Manager 创建或桥接 Swarm Run,并保存 `deployment_id`、`runtime_deployment_id`、`swarm_id`。 +3. Runtime 真实生成任务图,并回传任务状态、Agent 状态、handoff、artifact、审批请求。 +4. Manager 对 callback 去重、落库、脱敏,并能按 deployment/timeline/artifact 查询。 +5. 高危审批由客户端展示并提交,Manager 记录,Runtime 收到决定后继续或停止。 +6. 交付物能回到 Manager 和客户端,不能只停留在 Runtime Redis 或 Agent 日志里。 +7. 全链路日志和页面不得出现长期明文密钥、模型 key、云 access key、私钥或连接串。 + +## 六、下一步建议 + +| 顺序 | 动作 | 目标 | +|---:|---|---| +| 1 | 把 `docs/heicode-manager-sub-swarm-progress-checklist.md` 发给蜂群侧确认 | 让对方知道 Manager 已有什么、需要他们回什么 | +| 2 | 确认蜂群侧最终入口是 `/api/swarms` 还是先兼容 `/tasks` | 避免双方接口错位 | +| 3 | 用 Manager callback 接口跑一次蜂群侧真实 `artifact.created` / `approval.requested` | 证明回调、artifact、approval、timeline 有真实数据 | +| 4 | 客户端按 `docs/integration/heicode-desktop-sub-agile-api.md` 接入审批和 timeline | 跑通用户主体验 | +| 5 | 根据真实联调结果更新本文状态 | 把缺失项从“缺接口/未联调”改为“已验证/阻塞/延期” | diff --git a/docs/integration/agent-manager-swarm-runtime-change-request.md b/docs/integration/agent-manager-swarm-runtime-change-request.md new file mode 100644 index 00000000..e8146c7a --- /dev/null +++ b/docs/integration/agent-manager-swarm-runtime-change-request.md @@ -0,0 +1,403 @@ +# Agent Manager / HeiCode-Swarm 蜂群对接改造请求 + +更新时间:2026-05-27 +发起方:Heicode Manager +用途:给 Agent Manager / HeiCode-Swarm 侧确认需要补齐或对齐的生产接口,便于 Heicode 桌面客户端 -> Manager -> 蜂群 Runtime 跑完整蜂群流程。 + +## 一、当前 Manager 已准备好的能力 + +| 能力 | Manager 状态 | +|---|---| +| 用户态创建 | `POST /api/agnet/user/deployments`、`POST /api/swarms` 已可创建 Manager deployment | +| Runtime 创建桥接 | 可通过 `AGNET_RUNTIME_BASE_URL` + `AGNET_RUNTIME_CREATE_PATH` 调用 Runtime | +| Runtime 停止桥接 | 可通过 `AGNET_RUNTIME_STOP_PATH` 停止 Runtime deployment | +| 回调接收 | `POST /api/agnet/callbacks/swarm-events` 已支持事件、artifact、approval、timeline 回调 | +| 回调安全 | 支持 `X-Agnet-Service-Token` 或 `X-Agnet-Signature` HMAC | +| 幂等 | `event_id` / `idempotency_key` 去重 | +| 审批结果回传 | 用户 approve/reject 后,Manager 可调用 Runtime approval decision 接口 | +| 密钥边界 | Manager 只传 `secret_ref` / `credential_ref`,不传长期明文密钥 | + +## 二、Runtime 需要提供的接口 + +### 1. 健康检查 + +```http +GET /api/agnet/health +``` + +如果蜂群侧沿用现有 Orchestrator,也可以临时配置为: + +```http +GET /health +``` + +响应建议: + +```json +{ + "success": true, + "data": { + "status": "healthy", + "service": "heicode-swarm-runtime", + "version": "x.y.z" + } +} +``` + +### 2. 创建 Swarm Run + +生产目标接口: + +```http +POST /api/swarms +Authorization: Bearer +X-Correlation-ID: +X-Idempotency-Key: manager- +Content-Type: application/json +``` + +Manager 当前也可兼容配置: + +```text +AGNET_RUNTIME_CREATE_PATH=/api/swarms +``` + +请求核心字段: + +```json +{ + "orchestration_plan": { + "sub_mode": "agile", + "risk_level": "high", + "user_context": { + "user_id": "22", + "channel_id": "heicode" + }, + "billing_context": { + "provider": "newapi", + "default_model_id": "model_xxx", + "allowed_model_ids": ["model_xxx"], + "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/model-gateway-key" + }, + "agents": [], + "resource_grants": [] + }, + "agents": [ + { + "role": "backend", + "resource_grants": [] + } + ], + "resource_grants": [ + { + "grant_id": "grant-task-backend", + "resource_type": "git", + "permission_scope": ["read", "write"], + "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/repo-main" + } + ], + "callback": { + "url": "https://code.xinghanlab.com/api/agnet/callbacks/swarm-events", + "signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agnet-callback-signing-key", + "subscribed_events": [ + "deployment.status_changed", + "task.created", + "task.claimed", + "task.heartbeat", + "task.completed", + "task.failed", + "handoff.requested", + "approval.requested", + "artifact.created", + "timeline.updated" + ] + }, + "metadata": { + "manager_deployment_id": "dep_xxx", + "heicode_runtime_bridge": true, + "correlation_id": "corr_xxx" + } +} +``` + +响应必须返回至少一个 Runtime 标识: + +```json +{ + "success": true, + "data": { + "deployment_id": "runtime-dep-123", + "swarm_id": "swarm-123", + "status": "created" + } +} +``` + +兼容要求: + +1. `deployment_id` 可作为 Runtime deployment id。 +2. `swarm_id` 用于蜂群任务图、审批和事件关联。 +3. 如果 Runtime 只返回顶层 JSON,不包 `data`,Manager 也能解析。 +4. 不要要求 Manager 发送长期明文密钥。 + +### 3. 停止 Runtime Run + +默认路径: + +```http +POST /api/agnet/deployments/{deployment_id}/stop +Authorization: Bearer +Content-Type: application/json +``` + +请求: + +```json +{ + "reason": "Heicode Manager requested stop", + "manager_deployment_id": "dep_xxx" +} +``` + +响应: + +```json +{ + "success": true, + "data": { + "deployment_id": "runtime-dep-123", + "swarm_id": "swarm-123", + "status": "stopped" + } +} +``` + +如果蜂群侧希望使用: + +```http +POST /api/swarms/{swarm_id}/stop +``` + +则 Manager 可通过环境变量配置: + +```text +AGNET_RUNTIME_STOP_PATH=/api/swarms/{swarm_id}/stop +``` + +## 三、Runtime 回调 Manager + +统一回调入口: + +```http +POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events +X-Agnet-Service-Token: +X-Agnet-Event-Id: +X-Correlation-ID: +Content-Type: application/json +``` + +也支持 HMAC: + +```http +X-Agnet-Timestamp: +X-Agnet-Signature: sha256= +``` + +签名内容: + +```text +timestamp + "." + event_id + "." + raw_body +``` + +### 通用事件 envelope + +```json +{ + "event_id": "evt_123", + "idempotency_key": "evt_123", + "event_type": "task.completed", + "deployment_id": "dep_xxx", + "swarm_id": "swarm-123", + "agent_instance_id": "agent-backend-1", + "task_id": "task-backend-1", + "occurred_at": "2026-05-27T10:00:00Z", + "correlation_id": "corr_xxx", + "source": "heicode-swarm-runtime", + "payload": {} +} +``` + +必须支持的事件类型: + +| event_type | 用途 | +|---|---| +| `deployment.status_changed` | Runtime 整体状态变化 | +| `task.created` | 任务图新增任务 | +| `task.claimed` | Agent claim 任务 | +| `task.heartbeat` | Agent 执行心跳 | +| `task.completed` | 任务完成 | +| `task.failed` | 任务失败 | +| `handoff.requested` | 任务交接 | +| `handoff.completed` | 交接完成 | +| `approval.requested` | 请求用户审批 | +| `artifact.created` | 产物生成 | +| `timeline.updated` | 用户可见时间线 | + +### artifact 回调 + +```json +{ + "event_id": "evt_artifact_1", + "event_type": "artifact.created", + "deployment_id": "dep_xxx", + "swarm_id": "swarm-123", + "task_id": "task-backend-1", + "artifact": { + "artifact_id": "art_backend_patch_1", + "artifact_type": "code_patch", + "title": "Backend patch", + "summary": "完成后端接口修改", + "uri": "git://repo#agent-backend-1-results", + "checksum": "sha256:xxx", + "metadata": { + "commit_sha": "abc123", + "branch": "agent-backend-1-results", + "redacted": true + } + } +} +``` + +### approval 请求回调 + +```json +{ + "event_id": "evt_approval_1", + "event_type": "approval.requested", + "deployment_id": "dep_xxx", + "swarm_id": "swarm-123", + "task_id": "task-deploy-1", + "payload": { + "approval_id": "runtime-approval-1", + "operation": "production_deploy", + "resource_id": "azure-prod", + "resource_type": "cloud_resource", + "resource_scope": "/subscriptions/.../resourceGroups/prod", + "target_role": "ops", + "risk_level": "high", + "requires_credential": true, + "secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/azure-prod-sp", + "ttl_seconds": 900, + "reason": "部署到生产环境需要用户确认" + } +} +``` + +## 四、Manager 回传审批结果给 Runtime + +Manager 已支持调用该接口。蜂群侧需要实现接收。 + +默认路径: + +```http +POST /api/swarms/{swarm_id}/approvals/{approval_id} +Authorization: Bearer +X-Correlation-ID: +X-Idempotency-Key: approval-decision-- +Content-Type: application/json +``` + +Manager 可配置: + +```text +AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id} +``` + +请求: + +```json +{ + "approval_id": "runtime-approval-1", + "decision": "approved", + "manager_deployment_id": "dep_xxx", + "runtime_deployment_id": "runtime-dep-123", + "swarm_id": "swarm-123", + "operation": "production_deploy", + "resource_id": "azure-prod", + "resource_type": "cloud_resource", + "resource_scope": "/subscriptions/.../resourceGroups/prod", + "target_role": "ops", + "risk_level": "high", + "requires_credential": true, + "credential_ref": "lease://agnet/lease_xxx", + "lease_id": "lease_xxx", + "lease_expires_at": 1770000000000, + "decided_by": "user:22", + "reason": "用户确认部署", + "decided_at": 1770000000000 +} +``` + +拒绝时: + +```json +{ + "approval_id": "runtime-approval-1", + "decision": "rejected", + "manager_deployment_id": "dep_xxx", + "runtime_deployment_id": "runtime-dep-123", + "swarm_id": "swarm-123", + "reason": "风险过高", + "decided_by": "user:22", + "decided_at": 1770000000000 +} +``` + +安全要求: + +1. Manager 不会在该请求中发送 `secret_ref`。 +2. 如果审批通过且需要凭证,只发送短期 `credential_ref = lease://agnet/...`。 +3. Runtime 不得把 `credential_ref`、service token、签名密钥写入日志或 artifact metadata。 +4. Runtime 收到 `approved` 后继续原高危动作;收到 `rejected` 后停止该动作并回调 `timeline.updated` 或 `task.failed/blocked`。 + +响应: + +```json +{ + "success": true, + "data": { + "approval_id": "runtime-approval-1", + "status": "accepted" + } +} +``` + +## 五、当前 HeiCode-Swarm 现状与建议 + +当前 `HeiCode-Swarm` 可见 Orchestrator 入口是: + +| 当前入口 | 可用性 | 建议 | +|---|---|---| +| `GET /health` | 可直接作为健康检查 | Manager `AGNET_RUNTIME_HEALTH_PATH=/health` | +| `POST /tasks` | 可临时创建任务 | 只能做兼容桥接,不建议作为最终生产契约 | +| `GET /tasks` | 可查任务 | 后续需要映射到 callback/timeline | +| `GET /agents` | 可查 Agent | 后续需要回调 agent status | +| `GET /handoffs` | 可查 handoff | 后续需要回调 handoff events | +| `GET /metrics` | 可查指标 | 后续需要按 deployment/swarm 关联 | +| `WS /ws/{agent_id}` | Agent 内部通道 | Manager 不直接接 WS | + +建议蜂群侧优先补正式 `POST /api/swarms` 和 approval decision 接口;如果短期只想先跑通,可由 Manager 临时把 create path 指到 `/tasks`,但这会缺少 `secret_ref`、resource grants、approval policy、artifact callback 等正式字段。 + +## 六、联调验收 + +最小验收顺序: + +1. Manager 调 `GET /api/agnet/health` 或 `/health` 返回 healthy。 +2. Manager 调 Runtime create,拿到 `runtime_deployment_id` 和 `swarm_id`。 +3. Runtime 回调 `deployment.status_changed`,Manager timeline 可见。 +4. Runtime 回调 `task.claimed`、`task.completed`,Manager timeline 可见。 +5. Runtime 回调 `artifact.created`,Manager artifacts 可见。 +6. Runtime 回调 `approval.requested`,Manager/客户端能看到 pending approval。 +7. 用户 approve/reject 后,Runtime 收到 approval decision。 +8. Runtime 根据 decision 继续或停止,并回调最终结果。 +9. 全链路无明文长期密钥、模型 key、云 access key、私钥、连接串。 diff --git a/docs/integration/heicode-desktop-sub-agile-api.md b/docs/integration/heicode-desktop-sub-agile-api.md index 340232d0..95d2ba52 100644 --- a/docs/integration/heicode-desktop-sub-agile-api.md +++ b/docs/integration/heicode-desktop-sub-agile-api.md @@ -1221,7 +1221,7 @@ setInterval(async () => { ## 15. 当前生产注意事项 -1. `https://code.xinghanlab.com` 的 Manager 用户态接口已上线,当前生产版本为 `1.4.7`。 +1. `https://code.xinghanlab.com` 的 Manager 用户态接口已上线,当前生产版本为 `1.4.8`。 2. Manager 本地控制面可创建 `sub_mode=agile/waterfall` deployment。 3. 生产 Manager 已配置 Agent Manager Runtime,当前直接走 `http://20.212.121.126`;域名和 HTTPS 后续单独处理,不作为客户端当前接入阻塞项。 4. V2 加密 `deployment-draft` 已在生产验证通过:真实构造 `Content-Encoding: heicode-aead-v1` 请求返回 200,`sub_mode=agile`,`user_id=22`。 diff --git a/docs/integration/heicode-manager-swarm-runtime-env-template.md b/docs/integration/heicode-manager-swarm-runtime-env-template.md new file mode 100644 index 00000000..6281d5f3 --- /dev/null +++ b/docs/integration/heicode-manager-swarm-runtime-env-template.md @@ -0,0 +1,151 @@ +# Heicode Manager 蜂群 Runtime 联调配置模板 + +更新时间:2026-05-27 +适用范围:Heicode Manager 调用 Agent Manager / HeiCode-Swarm Runtime。 + +## 一、标准 Agent Manager Runtime 配置 + +当 Runtime 提供正式 `POST /api/swarms` 时,Manager 建议配置: + +```bash +AGNET_RUNTIME_ENABLED=true +AGNET_RUNTIME_ASYNC=false +AGNET_RUNTIME_BASE_URL=http:// +AGNET_RUNTIME_SERVICE_TOKEN= +AGNET_RUNTIME_HEALTH_PATH=/api/agnet/health +AGNET_RUNTIME_CREATE_PATH=/api/swarms +AGNET_RUNTIME_STOP_PATH=/api/swarms/{swarm_id}/stop +AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id} +AGNET_RUNTIME_TIMEOUT_SECONDS=10 +HEICODE_PUBLIC_BASE_URL=https://code.xinghanlab.com +AGNET_RUNTIME_CALLBACK_URL=https://code.xinghanlab.com/api/agnet/callbacks/swarm-events +AGNET_CALLBACK_TOKEN= +``` + +说明: + +1. `AGNET_RUNTIME_SERVICE_TOKEN` 是 Manager 调 Runtime 的 Bearer token。 +2. `AGNET_CALLBACK_TOKEN` 是 Runtime 回调 Manager 时放在 `X-Agnet-Service-Token` 的 token。 +3. 两个 token 可以不同,不要写进前端或日志。 +4. 如果 Runtime 返回 `swarm_id`,Manager 会保存到 `runtime_swarm_id`。 +5. 如果 Runtime 返回 `deployment_id`,Manager 会保存到 `runtime_deployment_id`。 + +## 二、当前 HeiCode-Swarm Orchestrator 兼容配置 + +如果短期只用现有 Orchestrator 的 `/health` 和 `/tasks` 先做连通性测试,可以临时配置: + +```bash +AGNET_RUNTIME_ENABLED=true +AGNET_RUNTIME_ASYNC=false +AGNET_RUNTIME_BASE_URL=http://52.139.240.116:8000 +AGNET_RUNTIME_SERVICE_TOKEN= +AGNET_RUNTIME_HEALTH_PATH=/health +AGNET_RUNTIME_CREATE_PATH=/tasks +AGNET_RUNTIME_STOP_PATH=/tasks/{runtime_deployment_id}/stop +AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id} +AGNET_RUNTIME_TIMEOUT_SECONDS=10 +HEICODE_PUBLIC_BASE_URL=https://code.xinghanlab.com +AGNET_RUNTIME_CALLBACK_URL=https://code.xinghanlab.com/api/agnet/callbacks/swarm-events +AGNET_CALLBACK_TOKEN= +``` + +注意: + +1. `/tasks` 不是最终生产契约,只能做临时兼容测试。 +2. 当前 `/tasks` 字段不足以完整表达 `resource_grants`、`secret_ref`、`approval_policy`、`budget`。 +3. 若 Orchestrator 不支持 stop 和 approval decision,Manager 会记录回传失败审计,但用户审批记录仍会保存。 + +## 三、Runtime 回调 Manager 的请求头 + +推荐使用 service token: + +```http +POST /api/agnet/callbacks/swarm-events +X-Agnet-Service-Token: +X-Agnet-Event-Id: +X-Correlation-ID: +Content-Type: application/json +``` + +也支持 HMAC: + +```http +X-Agnet-Event-Id: +X-Agnet-Timestamp: +X-Agnet-Signature: sha256= +X-Correlation-ID: +Content-Type: application/json +``` + +签名内容: + +```text +timestamp + "." + event_id + "." + raw_body +``` + +## 四、最小联调命令 + +健康检查: + +```bash +curl -sS "$AGNET_RUNTIME_BASE_URL$AGNET_RUNTIME_HEALTH_PATH" +``` + +Manager 本地默认模拟链路: + +```bash +curl -sS -X POST "https://code.xinghanlab.com/api/agnet/user/deployments//simulate-events" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +该命令用于 Manager 自测,不代表真实 Runtime 已执行。默认模拟会落 callback、artifact、approval、timeline 记录,并用于检查页面展示和 `secret_ref` 脱敏。 + +模拟 Runtime 回调 Manager: + +```bash +curl -sS -X POST "https://code.xinghanlab.com/api/agnet/callbacks/swarm-events" \ + -H "Content-Type: application/json" \ + -H "X-Agnet-Service-Token: $AGNET_CALLBACK_TOKEN" \ + -H "X-Agnet-Event-Id: evt-smoke-001" \ + -H "X-Correlation-ID: corr-smoke-001" \ + -d '{ + "event_id": "evt-smoke-001", + "event_type": "timeline.updated", + "deployment_id": "", + "swarm_id": "", + "occurred_at": "2026-05-27T10:00:00Z", + "source": "heicode-swarm-runtime", + "payload": { + "title": "Runtime callback received", + "summary": "Manager callback pipeline is reachable", + "severity": "info" + } + }' +``` + +成功响应: + +```json +{ + "success": true, + "data": { + "event_id": "evt-smoke-001", + "inserted": true, + "deduplicated": false, + "deployment_id": "" + } +} +``` + +## 五、上线前必须确认 + +| 项 | 标准 | +|---|---| +| create | Manager 能拿到 `runtime_deployment_id` 和 `runtime_swarm_id` | +| callback | Runtime 至少回调 `deployment.status_changed`、`task.completed`、`artifact.created` | +| approval | Runtime 发 `approval.requested`,Manager 用户审批后 Runtime 收到 decision | +| artifact | Manager artifacts 页面能看到 artifact 类型、标题、摘要和 URI | +| timeline | Manager timeline 能区分 `manager`、`runtime`、`simulated` 来源 | +| secret | 请求、响应、日志、artifact metadata 中无长期明文密钥 | diff --git a/heicode/VERSION b/heicode/VERSION index be05bba9..b2e46d18 100644 --- a/heicode/VERSION +++ b/heicode/VERSION @@ -1 +1 @@ -1.4.7 +1.4.8 diff --git a/heicode/controller/agnet_approval.go b/heicode/controller/agnet_approval.go index 41cae89a..22f3a6aa 100644 --- a/heicode/controller/agnet_approval.go +++ b/heicode/controller/agnet_approval.go @@ -244,6 +244,7 @@ func ApproveAgnetApprovalRequest(c *gin.Context) { if lease != nil { recordAgnetApprovalAudit("credential_lease.created", &approval, lease, "ok", "") } + syncAgnetRuntimeApprovalDecision(c, &approval, lease, agnetApprovalStatusApproved) common.ApiSuccess(c, agnetApprovalToResponse(approval, lease)) } @@ -272,6 +273,7 @@ func RejectAgnetApprovalRequest(c *gin.Context) { return } recordAgnetApprovalAudit("approval.rejected", &approval, nil, "ok", "") + syncAgnetRuntimeApprovalDecision(c, &approval, nil, agnetApprovalStatusRejected) common.ApiSuccess(c, agnetApprovalToResponse(approval, nil)) } diff --git a/heicode/controller/agnet_approval_test.go b/heicode/controller/agnet_approval_test.go index 1587eab0..03089db4 100644 --- a/heicode/controller/agnet_approval_test.go +++ b/heicode/controller/agnet_approval_test.go @@ -3,6 +3,7 @@ package controller import ( "bytes" "fmt" + "io" "net/http" "net/http/httptest" "strings" @@ -33,6 +34,7 @@ func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB { &model.AgnetApprovalRequest{}, &model.AgnetCredentialLease{}, &model.AgnetAuditEvent{}, + &model.AgnetDeployment{}, )) t.Cleanup(func() { sqlDB, err := db.DB() @@ -43,6 +45,70 @@ func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB { return db } +func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) { + db := setupAgnetApprovalTestDB(t) + var runtimeBody string + var runtimeAuth string + var runtimePath string + runtimeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimePath = r.URL.Path + runtimeAuth = r.Header.Get("Authorization") + body, _ := io.ReadAll(r.Body) + runtimeBody = string(body) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"success":true,"data":{"status":"accepted"}}`)) + })) + defer runtimeServer.Close() + t.Setenv("AGNET_RUNTIME_ENABLED", "true") + t.Setenv("AGNET_RUNTIME_BASE_URL", runtimeServer.URL) + t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "runtime-token") + t.Setenv("AGNET_RUNTIME_APPROVAL_DECISION_PATH", "/api/swarms/{swarm_id}/approvals/{approval_id}") + + require.NoError(t, db.Create(&model.AgnetDeployment{ + DeploymentID: "dep_runtime_approval", + UserID: "7", + BindingScope: "project-alpha", + CorrelationID: "corr-runtime-approval", + RuntimeDeploymentID: "runtime-dep-1", + RuntimeSwarmID: "swarm-runtime-1", + Status: "running", + RuntimeState: "running", + PlanJSON: `{"user_context":{"user_id":"7"},"metadata":{"correlation_id":"corr-runtime-approval"}}`, + }).Error) + + body := `{ + "deployment_id":"dep_runtime_approval", + "binding_scope":"project-alpha", + "operation":"write_repository", + "resource_id":"repo-main", + "resource_type":"git", + "resource_scope":"https://example.invalid/acme/repo#main", + "target_role":"backend", + "risk_level":"high", + "requires_credential":true, + "secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main", + "ttl_seconds":600 + }` + + _, approvalID := createAgnetApprovalForTest(t, 7, body) + w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许写入"}`) + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "/api/swarms/swarm-runtime-1/approvals/"+approvalID, runtimePath) + require.Equal(t, "Bearer runtime-token", runtimeAuth) + require.Contains(t, runtimeBody, `"decision":"approved"`) + require.Contains(t, runtimeBody, `"approval_id":"`+approvalID+`"`) + require.Contains(t, runtimeBody, `"manager_deployment_id":"dep_runtime_approval"`) + require.Contains(t, runtimeBody, `"runtime_deployment_id":"runtime-dep-1"`) + require.Contains(t, runtimeBody, `"swarm_id":"swarm-runtime-1"`) + require.Contains(t, runtimeBody, `"credential_ref":"lease://agnet/`) + require.NotContains(t, runtimeBody, "azkv://") + + var auditRows []model.AgnetAuditEvent + require.NoError(t, db.Where("deployment_id = ?", "dep_runtime_approval").Order("id asc").Find(&auditRows).Error) + require.NotEmpty(t, auditRows) + require.Equal(t, "runtime.approval_decision.accepted", auditRows[len(auditRows)-1].Event) +} + func performAgnetApprovalRequest(handler gin.HandlerFunc, userID int, method string, routePath string, requestPath string, body string) *httptest.ResponseRecorder { r := gin.New() r.Handle(method, routePath, func(c *gin.Context) { diff --git a/heicode/controller/agnet_control_plane.go b/heicode/controller/agnet_control_plane.go index 0195489f..89110616 100644 --- a/heicode/controller/agnet_control_plane.go +++ b/heicode/controller/agnet_control_plane.go @@ -1311,6 +1311,194 @@ func normalizeAgnetSimulationEvents(values []string) []string { return events } +func firstAgnetSimulationGrant(record agnetDeploymentRecord) agnetResourceGrant { + for _, agent := range record.Plan.Agents { + for _, grant := range agent.ResourceGrants { + if strings.TrimSpace(grant.SecretRef) != "" { + return grant + } + } + } + for _, grant := range record.Plan.ResourceGrants { + if strings.TrimSpace(grant.SecretRef) != "" { + return grant + } + } + for _, agent := range record.Plan.Agents { + if len(agent.ResourceGrants) > 0 { + return agent.ResourceGrants[0] + } + } + if len(record.Plan.ResourceGrants) > 0 { + return record.Plan.ResourceGrants[0] + } + return agnetResourceGrant{} +} + +func firstAgnetSimulationRole(record agnetDeploymentRecord) string { + for _, agent := range record.Plan.Agents { + if strings.TrimSpace(agent.RoleTemplate) != "" { + return strings.TrimSpace(agent.RoleTemplate) + } + } + for _, agent := range record.Plan.AgentRuntime.Agents { + if strings.TrimSpace(agent.Role) != "" { + return strings.TrimSpace(agent.Role) + } + } + return "runtime" +} + +func persistAgnetSimulatedCallback(record agnetDeploymentRecord, payload agnetCallbackEnvelope) error { + if strings.TrimSpace(payload.EventID) == "" { + payload.EventID = "evt-sim-" + common.GetUUID()[:12] + } + if strings.TrimSpace(payload.IdempotencyKey) == "" { + payload.IdempotencyKey = payload.EventID + } + if strings.TrimSpace(payload.DeploymentID) == "" { + payload.DeploymentID = record.DeploymentID + } + if strings.TrimSpace(payload.SwarmID) == "" { + payload.SwarmID = firstNonEmpty(record.RuntimeSwarmID, "sim-"+record.DeploymentID) + } + if strings.TrimSpace(payload.CorrelationID) == "" { + payload.CorrelationID = record.Plan.Metadata.CorrelationID + } + if strings.TrimSpace(payload.Source) == "" { + payload.Source = "agnet-simulator" + } + if strings.TrimSpace(payload.OccurredAt) == "" { + payload.OccurredAt = agnetNow() + } + payloadJSON, _ := common.Marshal(payload) + inserted, err := model.InsertAgnetCallbackEvent(&model.AgnetCallbackEvent{ + EventID: strings.TrimSpace(payload.EventID), + IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey), + CallbackType: "swarm-event", + EventType: strings.TrimSpace(payload.EventType), + DeploymentID: strings.TrimSpace(payload.DeploymentID), + SwarmID: strings.TrimSpace(payload.SwarmID), + AgentInstanceID: strings.TrimSpace(payload.AgentInstanceID), + TaskID: strings.TrimSpace(payload.TaskID), + UserID: record.Plan.UserContext.UserID, + BindingScope: firstPlanBindingScope(record.Plan), + CorrelationID: strings.TrimSpace(payload.CorrelationID), + Source: strings.TrimSpace(payload.Source), + Result: "simulated", + PayloadJSON: string(payloadJSON), + OccurredAt: strings.TrimSpace(payload.OccurredAt), + CreatedAtMs: time.Now().UnixMilli(), + }) + if err != nil { + return err + } + if !inserted { + return nil + } + if err := persistAgnetArtifactFromCallback(payload, record); err != nil { + return err + } + if err := persistAgnetApprovalFromCallback(payload, record); err != nil { + return err + } + recordAgnetAuditEvent(agnetEvent{ + EventID: "evt_" + common.GetUUID()[:12], + Event: "callback." + payload.EventType, + SchemaVersion: 1, + UserID: record.Plan.UserContext.UserID, + ChannelID: record.Plan.UserContext.ChannelID, + BindingScope: firstPlanBindingScope(record.Plan), + DeploymentID: record.DeploymentID, + CorrelationID: payload.CorrelationID, + OccurredAt: payload.OccurredAt, + }, "agnet_simulator", record.DeploymentID, "", "simulated") + return nil +} + +func persistAgnetDefaultSimulationCallbacks(record agnetDeploymentRecord) error { + role := firstAgnetSimulationRole(record) + grant := firstAgnetSimulationGrant(record) + taskID := "task-sim-" + sanitizeAgnetRef(record.DeploymentID) + agentID := "agi-sim-" + sanitizeAgnetRef(role) + base := func(eventType string, suffix string, payload map[string]any) agnetCallbackEnvelope { + return agnetCallbackEnvelope{ + EventID: "evt-sim-" + sanitizeAgnetRef(record.DeploymentID) + "-" + suffix, + IdempotencyKey: "idem-sim-" + sanitizeAgnetRef(record.DeploymentID) + "-" + suffix, + EventType: eventType, + DeploymentID: record.DeploymentID, + SwarmID: firstNonEmpty(record.RuntimeSwarmID, "sim-"+record.DeploymentID), + AgentInstanceID: agentID, + TaskID: taskID, + CorrelationID: record.Plan.Metadata.CorrelationID, + Source: "agnet-simulator", + OccurredAt: agnetNow(), + Payload: payload, + } + } + callbacks := []agnetCallbackEnvelope{ + base("task.claimed", "task-claimed", map[string]any{ + "title": "Simulated task claimed", + "summary": "Manager simulator marked a child Agnet task as claimed", + "agent_role": role, + "severity": "info", + }), + base("task.completed", "task-completed", map[string]any{ + "title": "Simulated task completed", + "summary": "Manager simulator marked the child Agnet task as completed", + "agent_role": role, + "severity": "success", + }), + } + artifact := base("artifact.created", "artifact", map[string]any{ + "title": "Simulated test report", + "summary": "Manager simulator generated a safe artifact record for smoke testing", + "checkpoint": "artifact_ready", + "severity": "success", + }) + artifact.Artifact = agnetArtifactPayload{ + ArtifactID: "art-sim-" + sanitizeAgnetRef(record.DeploymentID), + ArtifactType: "test_report", + Title: "Simulated test report", + Summary: "Manager simulator generated a safe artifact record for smoke testing", + URI: "artifact://simulated/" + record.DeploymentID + "/test-report", + Metadata: map[string]any{ + "source": "agnet-simulator", + "redacted": true, + }, + } + callbacks = append(callbacks, artifact) + if strings.TrimSpace(grant.SecretRef) != "" { + callbacks = append(callbacks, base("approval.requested", "approval", map[string]any{ + "approval_id": "sim-appr-" + sanitizeAgnetRef(record.DeploymentID), + "operation": "simulation.resource_access", + "resource_id": firstNonEmpty(grant.ResourceID, record.DeploymentID), + "resource_type": firstNonEmpty(grant.ResourceType, "custom"), + "resource_scope": firstNonEmpty(agnetGrantResourceRef(grant), firstPlanBindingScope(record.Plan)), + "target_role": firstNonEmpty(grant.TargetRole, role), + "risk_level": "high", + "requires_credential": true, + "secret_ref": strings.TrimSpace(grant.SecretRef), + "ttl_seconds": 600, + "reason": "Simulated high-risk resource access for Manager smoke testing", + })) + } + callbacks = append(callbacks, base("timeline.updated", "timeline", map[string]any{ + "title": "Simulated runtime timeline", + "summary": "Manager simulator generated task, artifact and approval records", + "stage": "testing", + "checkpoint": "ready_for_runtime", + "severity": "info", + "next_action": "connect_real_runtime", + })) + for _, callback := range callbacks { + if err := persistAgnetSimulatedCallback(record, callback); err != nil { + return err + } + } + return nil +} + func AgnetSimulateUserDeploymentEvents(c *gin.Context) { if c.GetInt("role") < common.RoleAdminUser && !common.GetEnvOrDefaultBool("AGNET_SIMULATION_ENABLED", false) { agnetError(c, "SIMULATION_DISABLED", "simulation endpoint is admin-only unless AGNET_SIMULATION_ENABLED=true") @@ -1361,6 +1549,13 @@ func AgnetSimulateUserDeploymentEvents(c *gin.Context) { OccurredAt: agnetNow(), }, "agnet_simulator", record.DeploymentID, agnetRequestID(c), "simulated") } + if len(req.Events) == 0 { + if err := persistAgnetDefaultSimulationCallbacks(record); err != nil { + common.SysLog("persistAgnetDefaultSimulationCallbacks: " + err.Error()) + agnetError(c, "SIMULATION_PERSIST_FAILED", "failed to persist simulated callback records") + return + } + } common.ApiSuccess(c, gin.H{ "deployment_id": record.DeploymentID, diff --git a/heicode/controller/agnet_control_plane_test.go b/heicode/controller/agnet_control_plane_test.go index ef291543..5df5060b 100644 --- a/heicode/controller/agnet_control_plane_test.go +++ b/heicode/controller/agnet_control_plane_test.go @@ -778,6 +778,65 @@ func TestAgnetUserDeploymentSimulationIsDisabledForRegularUserByDefault(t *testi require.Equal(t, "SIMULATION_DISABLED", envelope.Error.Code) } +func TestAgnetUserDeploymentDefaultSimulationCreatesRelatedRecords(t *testing.T) { + setupAgnetControlPlaneTestDB(t) + resetAgnetControlPlaneState(t) + + plan := baseAgnetResourceGrantPlan() + plan.UserContext.UserID = "7" + for idx := range plan.Agents[0].ResourceGrants { + plan.Agents[0].ResourceGrants[idx].UserID = "7" + } + createRecorder, createEnvelope := postAgnetCreateUserDeployment(t, 7, plan) + require.Equal(t, http.StatusOK, createRecorder.Code) + require.True(t, createEnvelope.Success) + var createBody map[string]any + require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody)) + deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string) + + simRecorder := httptest.NewRecorder() + simCtx, _ := gin.CreateTestContext(simRecorder) + simCtx.Set("id", 7) + simCtx.Set("role", common.RoleAdminUser) + simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}} + simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`)) + simCtx.Request.Header.Set("Content-Type", "application/json") + AgnetSimulateUserDeploymentEvents(simCtx) + require.Equal(t, http.StatusOK, simRecorder.Code) + require.Contains(t, simRecorder.Body.String(), `"simulated":true`) + + artifactRecorder := httptest.NewRecorder() + artifactCtx, _ := gin.CreateTestContext(artifactRecorder) + artifactCtx.Set("id", 7) + artifactCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}} + artifactCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments/"+deploymentID+"/artifacts", nil) + AgnetListUserDeploymentArtifacts(artifactCtx) + require.Equal(t, http.StatusOK, artifactRecorder.Code) + require.Contains(t, artifactRecorder.Body.String(), `"artifact_type":"test_report"`) + require.Contains(t, artifactRecorder.Body.String(), `"artifact_id":"art-sim-`) + + approvalRecorder := httptest.NewRecorder() + approvalCtx, _ := gin.CreateTestContext(approvalRecorder) + approvalCtx.Set("id", 7) + approvalCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/approvals?deployment_id="+deploymentID+"&status=pending", nil) + ListAgnetApprovalRequests(approvalCtx) + require.Equal(t, http.StatusOK, approvalRecorder.Code) + require.Contains(t, approvalRecorder.Body.String(), `"approval_id":"sim-appr-`) + require.Contains(t, approvalRecorder.Body.String(), `"status":"pending"`) + require.NotContains(t, approvalRecorder.Body.String(), "azkv://") + + timelineRecorder := httptest.NewRecorder() + timelineCtx, _ := gin.CreateTestContext(timelineRecorder) + timelineCtx.Set("id", 7) + timelineCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}} + timelineCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments/"+deploymentID+"/timeline", nil) + AgnetGetUserDeploymentTimeline(timelineCtx) + require.Equal(t, http.StatusOK, timelineRecorder.Code) + require.Contains(t, timelineRecorder.Body.String(), `"event_type":"artifact.created"`) + require.Contains(t, timelineRecorder.Body.String(), `"event_type":"approval.requested"`) + require.Contains(t, timelineRecorder.Body.String(), `"source":"agnet-simulator"`) +} + func TestAgnetUserSwarmsAdapterCreatesScopedDeployment(t *testing.T) { setupAgnetControlPlaneTestDB(t) resetAgnetControlPlaneState(t) diff --git a/heicode/controller/agnet_runtime_client.go b/heicode/controller/agnet_runtime_client.go index eaf4539e..3a8919ed 100644 --- a/heicode/controller/agnet_runtime_client.go +++ b/heicode/controller/agnet_runtime_client.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/heicode/manager/common" + "github.com/heicode/manager/model" ) const ( @@ -22,14 +23,15 @@ const ( ) type agnetRuntimeConfig struct { - Enabled bool - Async bool - BaseURL string - Token string - CreatePath string - HealthPath string - StopPath string - Timeout time.Duration + Enabled bool + Async bool + BaseURL string + Token string + CreatePath string + HealthPath string + StopPath string + ApprovalDecisionPath string + Timeout time.Duration } type agnetRuntimeSyncResult struct { @@ -45,14 +47,15 @@ func agnetRuntimeClientConfig() agnetRuntimeConfig { timeoutSec = 5 } return agnetRuntimeConfig{ - Enabled: common.GetEnvOrDefaultBool("AGNET_RUNTIME_ENABLED", false), - Async: common.GetEnvOrDefaultBool("AGNET_RUNTIME_ASYNC", true), - BaseURL: strings.TrimRight(strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_BASE_URL", "")), "/"), - Token: strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_SERVICE_TOKEN", "")), - CreatePath: common.GetEnvOrDefaultString("AGNET_RUNTIME_CREATE_PATH", "/api/agnet/deployments"), - HealthPath: common.GetEnvOrDefaultString("AGNET_RUNTIME_HEALTH_PATH", "/api/agnet/health"), - StopPath: common.GetEnvOrDefaultString("AGNET_RUNTIME_STOP_PATH", "/api/agnet/deployments/{deployment_id}/stop"), - Timeout: time.Duration(timeoutSec) * time.Second, + Enabled: common.GetEnvOrDefaultBool("AGNET_RUNTIME_ENABLED", false), + Async: common.GetEnvOrDefaultBool("AGNET_RUNTIME_ASYNC", true), + BaseURL: strings.TrimRight(strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_BASE_URL", "")), "/"), + Token: strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_SERVICE_TOKEN", "")), + CreatePath: common.GetEnvOrDefaultString("AGNET_RUNTIME_CREATE_PATH", "/api/agnet/deployments"), + HealthPath: common.GetEnvOrDefaultString("AGNET_RUNTIME_HEALTH_PATH", "/api/agnet/health"), + StopPath: common.GetEnvOrDefaultString("AGNET_RUNTIME_STOP_PATH", "/api/agnet/deployments/{deployment_id}/stop"), + ApprovalDecisionPath: common.GetEnvOrDefaultString("AGNET_RUNTIME_APPROVAL_DECISION_PATH", "/api/swarms/{swarm_id}/approvals/{approval_id}"), + Timeout: time.Duration(timeoutSec) * time.Second, } } @@ -359,12 +362,136 @@ func agnetRuntimeStopPath(cfg agnetRuntimeConfig, runtimeDeploymentID string) st return strings.ReplaceAll(path, "{deployment_id}", url.PathEscape(runtimeDeploymentID)) } +func agnetRuntimeStopPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord) string { + path := agnetRuntimeStopPath(cfg, strings.TrimSpace(record.RuntimeDeploymentID)) + replacer := strings.NewReplacer( + "{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)), + "{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)), + "{runtime_deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)), + "{manager_deployment_id}", url.PathEscape(strings.TrimSpace(record.DeploymentID)), + ) + return replacer.Replace(path) +} + +func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploymentRecord, approvalID string) string { + path := strings.TrimSpace(cfg.ApprovalDecisionPath) + if path == "" { + path = "/api/swarms/{swarm_id}/approvals/{approval_id}" + } + replacer := strings.NewReplacer( + "{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)), + "{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)), + "{deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)), + "{runtime_deployment_id}", url.PathEscape(strings.TrimSpace(record.RuntimeDeploymentID)), + "{manager_deployment_id}", url.PathEscape(strings.TrimSpace(record.DeploymentID)), + "{approval_id}", url.PathEscape(strings.TrimSpace(approvalID)), + ) + return replacer.Replace(path) +} + +func callAgnetRuntimeApprovalDecision(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, approval model.AgnetApprovalRequest, lease *model.AgnetCredentialLease, decision string) error { + endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeApprovalDecisionPath(cfg, record, approval.ApprovalID)) + if err != nil { + return err + } + payload := gin.H{ + "approval_id": approval.ApprovalID, + "decision": decision, + "manager_deployment_id": record.DeploymentID, + "runtime_deployment_id": record.RuntimeDeploymentID, + "swarm_id": record.RuntimeSwarmID, + "operation": approval.Operation, + "resource_id": approval.ResourceID, + "resource_type": approval.ResourceType, + "resource_scope": approval.ResourceScope, + "target_role": approval.TargetRole, + "risk_level": approval.RiskLevel, + "requires_credential": approval.RequiresCredential, + "decided_by": approval.DecidedBy, + "reason": approval.DecisionReason, + "decided_at": approval.DecidedAt, + } + if approval.UserId > 0 { + payload["user_id"] = fmt.Sprintf("%d", approval.UserId) + } + if lease != nil && strings.TrimSpace(lease.CredentialRef) != "" { + payload["credential_ref"] = strings.TrimSpace(lease.CredentialRef) + payload["lease_id"] = strings.TrimSpace(lease.LeaseID) + payload["lease_expires_at"] = lease.ExpiresAt + } + body, err := common.Marshal(payload) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Correlation-ID", firstNonEmpty(record.Plan.Metadata.CorrelationID, approval.ApprovalID)) + req.Header.Set("X-Idempotency-Key", "approval-decision-"+approval.ApprovalID+"-"+decision) + if cfg.Token != "" { + req.Header.Set("Authorization", "Bearer "+cfg.Token) + } + client := &http.Client{Timeout: cfg.Timeout} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + respBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if readErr != nil { + return readErr + } + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + return fmt.Errorf("runtime approval decision returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody))) + } + if len(respBody) == 0 { + return nil + } + var envelope map[string]any + if err := common.Unmarshal(respBody, &envelope); err != nil { + return err + } + if message := agnetRuntimeEnvelopeError(envelope); message != "" { + return errors.New(message) + } + return nil +} + +func syncAgnetRuntimeApprovalDecision(c *gin.Context, approval *model.AgnetApprovalRequest, lease *model.AgnetCredentialLease, decision string) { + if approval == nil || strings.TrimSpace(approval.DeploymentID) == "" { + return + } + cfg := agnetRuntimeClientConfig() + if !cfg.Enabled { + return + } + record, ok := findAgnetDeploymentRecord(approval.DeploymentID) + if !ok { + recordAgnetApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "deployment not found") + return + } + if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" { + recordAgnetApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "runtime identifiers missing") + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout) + defer cancel() + if err := callAgnetRuntimeApprovalDecision(ctx, cfg, record, *approval, lease, decision); err != nil { + recordAgnetApprovalAudit("runtime.approval_decision.failed", approval, lease, "failed", truncateAgnetFailureReason(err.Error())) + common.SysLog("Agnet runtime approval decision failed for " + approval.ApprovalID + ": " + err.Error()) + return + } + recordAgnetApprovalAudit("runtime.approval_decision.accepted", approval, lease, "ok", "") +} + func callAgnetRuntimeStop(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, reason string) (agnetRuntimeSyncResult, error) { runtimeDeploymentID := strings.TrimSpace(record.RuntimeDeploymentID) if runtimeDeploymentID == "" { return agnetRuntimeSyncResult{}, nil } - endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStopPath(cfg, runtimeDeploymentID)) + endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStopPathForRecord(cfg, record)) if err != nil { return agnetRuntimeSyncResult{}, err } diff --git a/heicode/web/default/src/features/agnet-console/api.ts b/heicode/web/default/src/features/agnet-console/api.ts index f12a0361..0fcd856d 100644 --- a/heicode/web/default/src/features/agnet-console/api.ts +++ b/heicode/web/default/src/features/agnet-console/api.ts @@ -159,6 +159,9 @@ export type AgnetDeployment = { status: string phase: string runtime_state?: string + runtime_deployment_id?: string + runtime_swarm_id?: string + runtime_last_sync_at?: string failure_reason?: string created_at: string updated_at: string diff --git a/heicode/web/default/src/features/agnet-console/pages.tsx b/heicode/web/default/src/features/agnet-console/pages.tsx index 2d4aed1c..6e0e7e94 100644 --- a/heicode/web/default/src/features/agnet-console/pages.tsx +++ b/heicode/web/default/src/features/agnet-console/pages.tsx @@ -9,6 +9,7 @@ import { CheckCircle2, CircleDashed, Coins, + Cpu, FileSearch, Filter, GitCommit, @@ -267,6 +268,35 @@ function describeScope(dep: AgnetDeployment): string { ) } +function compactRuntimeRef(value: string | undefined): string { + const trimmed = (value || '').trim() + if (!trimmed) return '—' + if (trimmed.length <= 18) return trimmed + return `${trimmed.slice(0, 10)}…${trimmed.slice(-6)}` +} + +function formatRecordSource(value: unknown): string { + const source = String(value || '').trim() + if (!source) return 'manager' + if (source.includes('simulate')) return 'simulated' + if (source.includes('runtime') || source.includes('swarm')) return 'runtime' + return source +} + +function artifactTypeToneClass(value: unknown): string { + const artifactType = String(value || '').toLowerCase() + if (artifactType.includes('patch') || artifactType.includes('code')) { + return 'bg-sky-500/15 text-sky-300 ring-sky-500/30' + } + if (artifactType.includes('test') || artifactType.includes('report')) { + return 'bg-emerald-500/15 text-emerald-300 ring-emerald-500/30' + } + if (artifactType.includes('deploy')) { + return 'bg-amber-500/15 text-amber-300 ring-amber-500/30' + } + return 'bg-muted/40 text-muted-foreground ring-border/60' +} + function collectResourceGrants( dep: AgnetDeployment ): Record[] { @@ -362,6 +392,9 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployment-events', dep.deployment_id], }) + void queryClient.invalidateQueries({ + queryKey: ['agnet', 'deployment-timeline', dep.deployment_id], + }) toast.success(t('Simulated events recorded')) }, onError: (err) => { @@ -434,6 +467,24 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) { /> +
+ + + +
+
@@ -968,12 +1019,27 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) { key={String(item.artifact_id || idx)} className='bg-muted/25 rounded-lg p-2' > -

- {String(item.title || item.artifact_id || t('Artifact'))} -

+
+

+ {String(item.title || item.artifact_id || t('Artifact'))} +

+ + {String(item.artifact_type || 'artifact')} + +

{String(item.summary || item.uri || '—')}

+ {Boolean(item.uri) && ( +

+ {String(item.uri)} +

+ )} ))} @@ -1007,6 +1073,42 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) { )}
+ +
+

+ {t('Merged timeline')} +

+ {timeline.length === 0 ? ( +

+ {t('No timeline records yet')} +

+ ) : ( +
    + {timeline.slice(0, 8).map((item, idx) => { + const event = String(item.event_type || item.event || 'event') + const source = formatRecordSource(item.source) + return ( +
  1. +
    +

    {event}

    + + {source} + +
    + {Boolean(item.title || item.summary || item.checkpoint) && ( +

    + {String(item.title || item.summary || item.checkpoint)} +

    + )} +
  2. + ) + })} +
+ )} +
) }