feat(agent): unify agnet→agent and implement client/runtime unification spec v0.1 core

按桌面客户端统一方案 v0.1 + agent_management Sub Mode Runtime 对接,强制全量统一,不留兼容。

命名统一(强制,无兼容):
- 全仓 agnet/Agnet/AGNET → agent/Agent/AGENT:后端 Go(路由 /api/agent/*、env AGENT_*、
  结构体/函数、19 个文件改名)、前端(agent-console/agent-hub、/api/agent 调用、i18n)、
  DB(表 agent_*、列 agent_id)、compose/.env、文档、脚本。
- DB 加幂等迁移 renameAgnetTablesToAgent():启动时 rename 老 agnet_* 表/列,保住生产数据。

统一方案核心(10 项):
- callback 统一 /api/agent/callbacks/runtime-events(路由/广播URL/函数名)。
- artifact 兜底判定改用 Runtime 权威信号 metadata.synthesized(§7.2)+ 结构化 artifact_type。
- Manager→Runtime 路径对齐 /api/agent/sub-agile/deployments(§2.2),{deployment_id} 回退 swarm_id。
- 状态裁决 display_status:Manager 唯一裁判,completed 无有效产物→needs_codegen/
  completed_without_deliverable(§10.6),接入 detail/timeline/workflow。
- GET /api/heicode/capabilities 能力发现(§6)。
- 模型策略 per_role(role_models)+ 收集 allowed_model_ids(§9)。
- resource_binding_id→secret_ref 服务端解析,客户端不再 inline secret_ref(§17.6)。
- 客户端统一路由层 /api/heicode/sub-agile|swarm/*(task≡deployment,复用控制面)+ workflow 投影。
- 日志分层 user_logs/debug_logs(§13)。

验证:go build ./... + go test(controller/router/model/middleware)全绿;前端 tsc -b + rsbuild build 通过。
待部署:VM .env 的 AGNET_*→AGENT_*;启动迁移自动 rename 表;其他三仓库需同步切到 /api/agent。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 23:45:10 +08:00
co-authored by Claude Opus 4.8
parent 12602ebcd6
commit 0fe1d20d67
112 changed files with 4305 additions and 3139 deletions
+2 -2
View File
@@ -9,7 +9,7 @@
| `cc-haha/` | **Heicode** 客户端:CLI(Ink)+ 本地 HTTP/WS 服务 + **Desktop**(Tauri + React) | Bun + TypeScript;产品入口 `bin/heicode` |
| `heicode/` | **Heicode Manager**:网关 + 管理控制台 | Go(Gin/GORM)+ `web/default` 前端(Bun/Rsbuild/React) |
| `website/` | 产品介绍站点 | Next.js;根 `docker-compose.yml` 提供 `heicode-www` :8888 |
| `docs/` | 愿景、**里程碑**(`docs/milestones/`)、**Agnet 集成**(`docs/integration/`) | Markdown |
| `docs/` | 愿景、**里程碑**(`docs/milestones/`)、**Agent 集成**(`docs/integration/`) | Markdown |
根 `package.json` 仅少量 workspace 级依赖(如适配器用到的包);**主要开发依赖在 `cc-haha/package.json`**。
@@ -51,7 +51,7 @@ HEICODE_TAIJIAICLOUD_BASE_URL=http://localhost:3000 bun run src/server/index.ts
1. **改哪一层跟哪篇文档**:Go 行为以 `heicode/AGENTS.md` 为准;客户端 TS/React 以 `cc-haha/AGENTS.md` 为准。
2. **小步提交**:沿用历史风格(如 `feat:` / `fix:` / `docs:`);PR 写清影响面与验证步骤。
3. **集成与「SK」边界**:平台契约见 [`docs/integration/agnet-platform-api-design.md`](./docs/integration/agnet-platform-api-design.md),里程碑见 `docs/milestones/`。
3. **集成与「SK」边界**:平台契约见 [`docs/integration/agent-platform-api-design.md`](./docs/integration/agent-platform-api-design.md),里程碑见 `docs/milestones/`。
4. **不要臆测计费**:计费与订阅在平台侧,不在 Heicode 客户端内实现。
## Docker / 站点
+2 -2
View File
@@ -9,7 +9,7 @@
| `cc-haha/` | **Heicode** 客户端:CLI(Ink)+ 本地 HTTP/WS 服务 + **Desktop**(Tauri + React) | Bun + TypeScript;产品入口 `bin/heicode` |
| `heicode/` | **Heicode Manager**:网关 + 管理控制台 | Go(Gin/GORM)+ `web/default` 前端(Bun/Rsbuild/React) |
| `website/` | 产品介绍站点 | Next.js;根 `docker-compose.yml` 提供 `heicode-www` :8888 |
| `docs/` | 愿景、**里程碑**(`docs/milestones/`)、**Agnet 集成**(`docs/integration/`) | Markdown |
| `docs/` | 愿景、**里程碑**(`docs/milestones/`)、**Agent 集成**(`docs/integration/`) | Markdown |
根 `package.json` 仅少量 workspace 级依赖(如适配器用到的包);**主要开发依赖在 `cc-haha/package.json`**。
@@ -51,7 +51,7 @@ HEICODE_TAIJIAICLOUD_BASE_URL=http://localhost:3000 bun run src/server/index.ts
1. **改哪一层跟哪篇文档**:Go 行为以 `heicode/CLAUDE.md` 为准;客户端 TS/React 以 `cc-haha/AGENTS.md` 为准。
2. **小步提交**:沿用历史风格(如 `feat:` / `fix:` / `docs:`);PR 写清影响面与验证步骤。
3. **集成与「SK」边界**:平台契约见 [`docs/integration/agnet-platform-api-design.md`](./docs/integration/agnet-platform-api-design.md),里程碑见 `docs/milestones/`。
3. **集成与「SK」边界**:平台契约见 [`docs/integration/agent-platform-api-design.md`](./docs/integration/agent-platform-api-design.md),里程碑见 `docs/milestones/`。
4. **不要臆测计费**:计费与订阅在平台侧,不在 Heicode 客户端内实现。
## Docker / 站点
@@ -0,0 +1,284 @@
# Heicode Manager 生产配置与账号交接清单
更新时间:2026-06-01
适用范围:Heicode Manager 生产环境、普通 sub Runtime、蜂群 Runtime、NewAPI、Azure 资源和后续 AI/运维交接。
## 0. 安全说明
本文只记录配置项、用途、位置和获取方式,不记录明文密码、Token、Key、连接串或私钥。
| 规则 | 要求 |
|------|------|
| 明文密钥 | 不写入 Markdown、git、聊天记录、日志、截图 |
| 生产 `.env` | 只保存在 VM 或部署平台 Secret 中,不提交仓库 |
| Azure Key Vault | 只保存长期密钥或 callback 签名密钥,代码和数据库只保存 `secret_ref` |
| 转交给其他 AI | 只给本文档;需要明文值时由负责人通过安全渠道单独提供 |
| 已泄漏密钥 | 一律按泄漏处理,立即轮换 |
## 0.1 配置可读取性结论
不是所有配置都能、也不应该在交接 Markdown 中读取和保存明文值。当前结论分三类:
| 分类 | 含义 | 处理方式 |
|------|------|----------|
| 可直接读取 | 仓库文档、compose、公开地址、非敏感环境变量能直接确认 | 在本文写出真实值 |
| 可确认已配置但不读取明文 | 生产容器 env 中存在,但值是密码、token、连接串或密钥 | 在本文写出配置名、用途、保存位置和 `<REDACTED>` |
| 当前无法确认明文或后台状态 | 需要 Azure Portal、NewAPI 后台、Gitee 凭据、VM root 权限或负责人授权 | 在本文明文写出“无法读取项名称、原因、获取入口” |
当前无法在本文保存明文的配置如下:
| 配置 / 凭据 | 是否能确认存在 | 为什么不能写明文 | 需要去哪里取 |
|-------------|----------------|------------------|--------------|
| VM SSH 密码/私钥 | 已知需要 | 登录凭据,写入文档会泄漏生产入口 | 负责人安全渠道、SSH keychain 或运维密码库 |
| Gitee 用户密码/token | 已知需要 | Git 凭据,写入文档会泄漏代码仓库权限 | 本机 credential helper、Gitee 个人 token、负责人安全渠道 |
| `SQL_DSN` | 已确认配置项存在 | 内含数据库用户名、密码、host、参数 | VM `.env`、容器 env、Azure PostgreSQL 连接串 |
| `REDIS_CONN_STRING` | 已确认配置项存在 | 内含 Redis 密码和 TLS 连接串 | VM `.env`、容器 env、Azure Redis Access keys |
| `SESSION_SECRET` | 已确认配置项存在 | 会话签名密钥,泄漏会影响登录安全 | VM `.env`、容器 env、部署平台 Secret |
| `CRYPTO_SECRET` | 已确认配置项存在 | Manager 加密密钥,泄漏会影响加密数据安全 | VM `.env`、容器 env、部署平台 Secret |
| `MCP_SERVER_INTERNAL_TOKEN` | 已确认配置项存在 | 内部服务调用 token | VM `.env`、容器 env |
| `AGENT_RUNTIME_SERVICE_TOKEN` | 已确认配置项存在 | Manager 调 Runtime 的 Bearer token | VM `.env`、容器 env、Agent Manager 负责人 |
| `AGENT_CALLBACK_TOKEN` | 已确认配置项存在 | Runtime 回调 Manager 的旧 token 兼容凭据 | VM `.env`、容器 env、Agent Manager 负责人 |
| `AGENT_CALLBACK_SIGNING_SECRET` | 已确认配置项存在 | Runtime 回调 Manager 的 HMAC 签名密钥 | VM `.env`、Key Vault `agent-callback-signing-key` |
| `AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF` | 已确认配置项存在 | secret_ref 本身不应暴露完整细节 | VM `.env`、Key Vault 配置 |
| NewAPI 管理员密码 | 当前未在仓库读取 | 后台管理员凭据 | NewAPI 后台、NewAPI DB、负责人安全渠道 |
| NewAPI 渠道 Key | 当前未在仓库读取 | 上游模型供应商 key | NewAPI 渠道管理页面或 DB |
| Azure Portal 账号/MFA | 当前未在仓库读取 | 云账号权限极高 | 负责人 Azure 账号体系 |
| Azure Key Vault secret value | 当前未直接读取 | Key Vault 密钥值只能由授权身份读取 | Azure Portal、Azure CLI、Managed Identity |
后续 AI 如果必须使用明文值,应只在当前 shell/session 的环境变量中临时使用,不能写入 Markdown、git diff、日志或截图。
## 1. 当前生产入口与代码仓库
| 项目 | 当前值 / 位置 | 说明 |
|------|---------------|------|
| Manager 生产地址 | `https://code.xinghanlab.com` | 用户和桌面客户端访问入口 |
| Manager 本地仓库 | `/Users/gongzhiyong/go/heicode-mananger` | 当前开发工作区 |
| Manager Git 远程 | `http://gitee.ath.cx:3000/taijibaga/heicode-mananger.git` | 继续开发、提交、上线使用 |
| 主分支 | `main` | 当前工作分支 |
| 旧 bug 仓库 | `HeiCode-issues.git` | 已废弃,后续不用再传 |
| 当前未跟踪文件 | `bak.txt` | 本地文件,不属于本交接文档内容 |
## 2. Azure / VM 生产资源
| 类型 | 当前配置 | 用途 | 备注 |
|------|----------|------|------|
| Azure Resource Group | `HEICODE` | Heicode 相关 Azure 资源组 | 以 Azure Portal 实际为准 |
| 区域 | `southeastasia` | 主要生产资源区域 | 以 Azure Portal 实际为准 |
| Manager VM | `heicode` | 当前承载 Manager Docker 容器 | 生产应用仍在 VM 上 |
| VM SSH Host | `20.24.50.121` | 登录生产 VM | 凭据见“账号与凭据清单” |
| VM SSH User | `heicode` | 生产 VM 运维用户 | 密码/私钥不写入本文 |
| Manager 容器 | `heicode` | 生产 Manager 应用容器 | 端口 `3000:3000` |
| Manager 镜像 | `heicode-manager:local` | VM 本地构建镜像 | 以 `docker ps` 为准 |
| 数据根目录 | `/var/lib/heicode/heicode` | bind mount 根目录 | 由 `HEICODE_DATA_ROOT` 控制 |
| 应用持久目录 | `/data` | 上传文件、桌面安装包、运行持久文件 | 容器内路径,映射到 VM |
| 应用日志目录 | `/app/logs` | Manager 日志 | 容器内路径,映射到 VM |
当前 VM 上观察到的容器:
| 容器 | 镜像 | 端口 | 说明 |
|------|------|------|------|
| `heicode` | `heicode-manager:local` | `3000:3000` | Manager 生产应用 |
| `new-api` | `calciumion/new-api:latest` | `3001:3000` | NewAPI 管理和模型网关 |
| `heicode-openbao` | `openbao/openbao:latest` | `127.0.0.1:8200:8200` | 历史/兼容密钥服务,不作为当前正式方案 |
| `postgres` | `postgres:15` | VM 内部 | VM 上存在容器,但 Manager 正式配置应以 Azure PostgreSQL 为准 |
| `redis` | `redis:latest` | VM 内部 | VM 上存在容器,但 Manager 正式配置应以 Azure Redis 为准 |
重要:仓库约定和部署文档要求生产业务库使用 Azure 托管 PostgreSQL / Redis,不要把 VM 上的 `postgres`、`redis` 容器误认为正式托管实例。
## 3. 当前 VM 关键路径
| 路径 | 用途 | 备注 |
|------|------|------|
| `/home/heicode/heicode/heicode/.env` | Manager 生产环境变量文件 | **当前真实部署目录**(compose `working_dir` 实锤);仓库远程为 `taijibaga/heicode-mananger` |
| `/home/heicode/heicode/heicode/docker-compose.azure-vm.yml` | Manager Azure VM compose | 当前 active compose(`docker inspect` 的 `config_files` 指向此文件) |
| `/home/heicode/apps/new-api/docker-compose.yml` | NewAPI compose | NewAPI 容器配置入口 |
| `/home/heicode/apps/_ABANDONED_heicode-git_xiaohei_20260601/` | **已废弃的旧 clone**(原 `xiaohei/heicode`,2026-06-01 改名挪走) | 不是部署源;曾误导排查。可后续硬删 |
| `/home/heicode/deploy/` | 历史部署目录 | 可能包含旧版本 `.env`,不要直接当作当前配置 |
| `/home/heicode/releases/` | 历史 release 目录 | 只用于回溯,不作为当前事实 |
| `/var/lib/heicode/heicode/data` | Manager 持久数据 | 对应容器 `/data` |
| `/var/lib/heicode/heicode/logs` | Manager 日志 | 对应容器 `/app/logs` |
## 4. Manager 基础环境变量
| 环境变量 | 当前状态 | 用途 | 值/来源 |
|----------|----------|------|---------|
| `TZ` | 已配置 | 时区 | `Asia/Shanghai` |
| `NODE_NAME` | 已配置 | 节点名 | `heicode-node-1` |
| `ERROR_LOG_ENABLED` | 已配置 | 错误日志开关 | `true` |
| `BATCH_UPDATE_ENABLED` | 已配置 | 批量更新开关 | `true` |
| `HEICODE_DATA_ROOT` | 已配置 | VM bind mount 根目录 | `/var/lib/heicode/heicode` |
| `SQL_DSN` | 已配置,敏感 | PostgreSQL 连接串 | VM `.env` / 容器 env,值 `<REDACTED>` |
| `REDIS_CONN_STRING` | 已配置,敏感 | Redis TLS 连接串 | VM `.env` / 容器 env,值 `<REDACTED>` |
| `SESSION_SECRET` | 已配置,敏感 | 登录会话签名 | VM `.env` / 容器 env,值 `<REDACTED>` |
| `CRYPTO_SECRET` | 已配置,敏感 | Manager 加密密钥 | VM `.env` / 容器 env,值 `<REDACTED>` |
| `MCP_SERVER_INTERNAL_TOKEN` | 已配置,敏感 | 内部服务调用 token | VM `.env` / 容器 env,值 `<REDACTED>` |
| `HEICODE_AUTH_BASE_URL` | 已配置 | Heicode/Agent 认证网关 | 默认参考 `https://apimtaiji.azure-api.net/api/mcp`,以 VM env 为准 |
| `HEICODE_ROOT_EMAILS` | 已配置 | JIT 自动 root 白名单 | 当前容器 env 可见 `xiaohei@qq.com` |
| `HEICODE_ADMIN_EMAILS` | 空 | JIT 自动 admin 白名单 | 当前未配置 |
生产数据库和 Redis:
| 服务 | 地址 | 说明 |
|------|------|------|
| Azure PostgreSQL | `heicode.postgres.database.azure.com` / DB `heicode` | Manager 正式数据库 |
| Azure Redis | `heicode.redis.cache.windows.net:6380` | TLS Redis,连接串走 `REDIS_CONN_STRING` |
## 5. Azure Key Vault / 密钥保管库配置
| 项目 | 当前状态 | 用途 | 备注 |
|------|----------|------|------|
| Key Vault 名称 | `heicode-kv` | 长期密钥和 callback 签名密钥托管 | 以 Azure Portal 为准 |
| Key Vault URL | `https://heicode-kv.vault.azure.net` | Manager 访问密钥保管库 | 对应 `AZURE_KEY_VAULT_URL` |
| `AZURE_KEY_VAULT_URL` | 已配置 | Key Vault 地址 | VM env,值应为上面 URL |
| `AZURE_CLIENT_ID` | 当前为空 | user-assigned managed identity client id | 若使用 system-assigned 可为空;若 health 报 Identity not found 需配置身份 |
| `agent-callback-signing-key` | 必须存在 | Runtime callback HMAC 签名密钥 | 建议通过 Key Vault 存储 |
当前已知问题:
| 问题 | 含义 | 处理方式 |
|------|------|----------|
| `Identity not found` | VM 当前没有可用 Managed Identity,或未绑定正确 `AZURE_CLIENT_ID` | 在 VM/容器服务开启 Managed Identity,并给 `heicode-kv` 授权 |
Key Vault 最低权限:
| 用途 | 权限 / RBAC |
|------|-------------|
| 读取 callback 签名密钥 | `secrets/get` |
| 健康检查列举 | `secrets/list` |
| Manager 写入资源密钥 PutSecret | `secrets/set` |
| RBAC 参考角色 | `Key Vault Secrets User` 或 `Key Vault Secrets Officer` |
## 6. 普通 sub Runtime / Agent Manager 配置
普通 sub 和蜂群是两套模式,不要混用。普通 sub 当前走 Agent Manager Runtime IP。
| 环境变量 | 当前状态 | 当前值 / 位置 | 说明 |
|----------|----------|---------------|------|
| `AGENT_RUNTIME_ENABLED` | 已启用 | `true` | 开启普通 sub Runtime |
| `AGENT_RUNTIME_BASE_URL` | 已配置 | `http://20.212.121.126` | 当前联调和生产使用 IP,域名未切换 |
| `AGENT_RUNTIME_HEALTH_PATH` | 已配置 | `/api/agent/health` | Runtime 健康检查 |
| `AGENT_RUNTIME_CREATE_PATH` | 已配置 | `/api/swarms` | 普通 sub 创建入口 |
| `AGENT_RUNTIME_STOP_PATH` | 已配置 | `/api/swarms/{swarm_id}/stop` | 停止 Runtime run |
| `AGENT_RUNTIME_APPROVAL_DECISION_PATH` | 已配置 | `/api/swarms/{swarm_id}/approvals/{approval_id}` | 审批结果回传 |
| `AGENT_RUNTIME_TIMEOUT_SECONDS` | 已配置 | `15` | Manager 调 Runtime 超时 |
| `AGENT_RUNTIME_SERVICE_TOKEN` | 已配置,敏感 | VM env,值 `<REDACTED>` | Manager 调 Runtime 的 Bearer token |
| `AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF` | 已配置,敏感引用 | VM env,值 `<REDACTED>` | callback HMAC secret_ref,正式应为 `azkv://...` |
| `AGENT_CALLBACK_TOKEN` | 已配置,敏感 | VM env,值 `<REDACTED>` | Runtime 回调 Manager 的旧 token 兼容 |
| `AGENT_CALLBACK_SIGNING_SECRET` | 已配置,敏感 | VM env,值 `<REDACTED>` | Runtime 回调 Manager 的 HMAC fallback |
普通 sub 当前标准链路:
```text
桌面客户端
-> Heicode Manager /api/agent/user/*
-> Agent Manager Runtime http://20.212.121.126/api/swarms
-> Runtime/Agent 执行
-> Runtime callback 到 https://code.xinghanlab.com/api/agent/callbacks/swarm-events
-> Manager 落库
-> 桌面客户端查询 timeline/logs/artifacts/content
```
## 7. 蜂群 Runtime 配置
蜂群模式和普通 sub 模式是两套部署、两套语义。Manager 当前环境里蜂群 Runtime 开关是关闭状态。
| 环境变量 | 当前状态 | 当前值 / 位置 | 说明 |
|----------|----------|---------------|------|
| `SWARM_RUNTIME_ENABLED` | 当前关闭 | `false` | 蜂群 Runtime 独立开关 |
| `SWARM_RUNTIME_BASE_URL` | 当前为空 | `<EMPTY>` | 蜂群 Runtime 地址未在 Manager 生产 env 启用 |
| `SWARM_RUNTIME_CREATE_PATH` | 已有默认 | `/api/swarms` | 蜂群创建入口默认值 |
| `SWARM_RUNTIME_HEALTH_PATH` | 已有默认 | `/api/agent/health` | 蜂群健康检查默认值 |
| `SWARM_RUNTIME_STOP_PATH` | 已有默认 | `/api/swarms/{swarm_id}/stop` | 蜂群停止默认值 |
| `SWARM_RUNTIME_APPROVAL_DECISION_PATH` | 已有默认 | `/api/swarms/{swarm_id}/approvals/{approval_id}` | 蜂群审批默认值 |
| `SWARM_RUNTIME_SERVICE_TOKEN` | 当前为空 | `<EMPTY>` | 蜂群 Runtime token 未配置 |
已知蜂群 Orchestrator 地址:
| 项目 | 地址 | 说明 |
|------|------|------|
| HeiCode-Swarm Orchestrator | `http://52.139.240.116:8000` | 蜂群项目独立 Runtime / Orchestrator |
## 8. NewAPI / 模型网关配置
| 项目 | 当前配置 | 用途 | 备注 |
|------|----------|------|------|
| NewAPI 容器 | `new-api` | 模型网关、渠道、模型 token、用量 | VM 上运行 |
| NewAPI 镜像 | `calciumion/new-api:latest` | NewAPI 服务镜像 | 以 `docker ps` 为准 |
| NewAPI VM 端口 | `20.24.50.121:3001` -> container `3000` | 管理后台或内部访问 | 是否公网开放以 NSG/防火墙为准 |
| NewAPI compose | `/home/heicode/apps/new-api/docker-compose.yml` | NewAPI 部署配置 | 明文密钥不得写入本文 |
| 模型渠道配置 | NewAPI 管理后台 / DB | 渠道 key、base_url、模型映射 | 不在 Manager 文档保存明文 |
| Manager 模型接口 | `https://code.xinghanlab.com/v1/...` | 桌面客户端模型调用入口 | Manager / NewAPI 转发 |
## 9. 账号与凭据清单
| 类别 | 标识 | 用途 | 明文值位置 / 获取方式 |
|------|------|------|-----------------------|
| VM SSH | host `20.24.50.121`, user `heicode` | 登录生产 VM、部署、看日志、清 Docker | 密码或私钥由负责人通过安全渠道提供,本文不保存 |
| Gitee Manager 仓库 | `http://gitee.ath.cx:3000/taijibaga/heicode-mananger.git` | 拉取、提交、推送 Manager 代码 | Gitee 用户名/密码或 token 由负责人提供;可能已在本机 credential helper |
| Manager Root 邮箱 | `xiaohei@qq.com` | 生产 root 白名单 | 只记录邮箱,不记录密码 |
| Manager 测试用户 | `zsbgnw@gmail.com` | 桌面客户端和 Manager 联调测试 | 密码不写入本文,需负责人单独提供 |
| Manager 管理后台用户 | 以生产数据库/后台为准 | 登录 `https://code.xinghanlab.com` 管理配置 | 密码不写入本文 |
| NewAPI 管理员 | 以 NewAPI 后台/DB 为准 | 配置渠道、模型、用户 token、日志 | 密码不写入本文 |
| NewAPI 渠道 Key | NewAPI 后台渠道管理 | 模型上游调用 | Key 不写入本文 |
| Azure Portal | 负责人 Azure 账号 | VM、Key Vault、PostgreSQL、Redis、网络配置 | 账号/密码/MFA 不写入本文 |
| Azure Key Vault Secret | `agent-callback-signing-key` | callback HMAC 签名 | 在 `heicode-kv` 获取/轮换 |
| Runtime Service Token | `AGENT_RUNTIME_SERVICE_TOKEN` | Manager 调 Agent Manager Runtime | VM env 或部署平台 Secret,值 `<REDACTED>` |
| Callback Token | `AGENT_CALLBACK_TOKEN` | Runtime 回调 Manager 旧 token 兼容 | VM env 或部署平台 Secret,值 `<REDACTED>` |
| Callback HMAC Secret | `AGENT_CALLBACK_SIGNING_SECRET` | Runtime 回调 Manager HMAC fallback | VM env / Key Vault,值 `<REDACTED>` |
| PostgreSQL 密码 | `SQL_DSN` 内 | Manager 数据库连接 | VM `.env` / Azure PostgreSQL,值 `<REDACTED>` |
| Redis 密码 | `REDIS_CONN_STRING` 内 | Manager Redis 连接 | VM `.env` / Azure Redis,值 `<REDACTED>` |
## 10. 常用排障命令
以下命令只列配置名或状态,避免直接打印明文密钥。
```bash
sudo docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
sudo docker inspect heicode \
--format '{{range .Config.Env}}{{println .}}{{end}}' \
| awk -F= '{print $1}' \
| sort
curl -fsS http://127.0.0.1:3000/api/status
curl -fsS https://code.xinghanlab.com/api/status
curl -fsS http://20.212.121.126/api/agent/health
```
部署 Manager:
```bash
cd /home/heicode/heicode/heicode # 真实部署目录(remote: taijibaga/heicode-mananger, 分支 main)
git pull
# 真实部署只有 docker-compose.azure-vm.yml + .env,无 docker-compose.override.yml
docker compose -f docker-compose.azure-vm.yml --env-file .env up -d --build
sudo docker image prune -f
```
## 11. 后续接手必须先确认的事项
| 序号 | 检查项 | 标准 |
|------|--------|------|
| 1 | 当前线上 commit | VM 工作树和 Gitee `main` 对齐 |
| 2 | Manager 容器健康 | `docker ps` 显示 `heicode` healthy,`/api/status` 返回成功 |
| 3 | 数据库连接 | `SQL_DSN` 指向 Azure PostgreSQL,不误用 VM 本地容器 |
| 4 | Redis 连接 | `REDIS_CONN_STRING` 指向 Azure Redis TLS,不误用 VM 本地容器 |
| 5 | Key Vault | `/api/secret-store/status` 不再报 `Identity not found` |
| 6 | 普通 sub | `AGENT_RUNTIME_BASE_URL=http://20.212.121.126`,创建、callback、artifact content 全链路可跑 |
| 7 | 蜂群 | `SWARM_RUNTIME_ENABLED` 当前为 `false`;如启用必须单独联调 |
| 8 | NewAPI | 模型渠道、token、日志正常;Runtime 失败时核对 `newapi_request_id` |
| 9 | 桌面客户端 | 加密 body、登录、模型列表、普通 sub artifact 展示正常 |
| 10 | 敏感值 | 任何 `.env`、token、密码不得进入 git diff |
## 12. 交接边界
下一位 AI 或工程师可以直接基于本文继续做:
- Manager 部署、版本更新、Docker 清理。
- 普通 sub Runtime 联调、artifact content 下载链路验证。
- 蜂群 Runtime 独立启用前的配置核对。
- NewAPI 模型调用排障。
- Azure Key Vault Managed Identity 修复。
- 桌面客户端对接文档更新。
但不能直接从本文获得明文密码或 token。需要真实密钥时,必须由负责人通过安全渠道提供,或在 VM/Azure Portal/NewAPI 后台按权限读取。
@@ -0,0 +1,208 @@
# Heicode Manager 项目说明与踩坑交接
更新时间:2026-06-01
用途:给下一位 AI / 工程师快速理解 Heicode Manager 的项目边界、生产拓扑、普通 sub、蜂群、NewAPI 和已踩过的坑。
## 1. 一句话说明
Heicode Manager 是 Heicode 的服务端控制面和网页管理台,负责用户登录、模型网关配置、用户/设备/模型/渠道管理、普通 sub 敏捷任务控制、Runtime callback 落库、artifact/timeline 展示,以及和桌面客户端、Agent Manager Runtime、NewAPI、Azure Key Vault 之间的编排。
它不是桌面客户端本体,也不是 Agent Runtime 本体,也不是 NewAPI 本体。
## 2. 仓库结构
| 路径 | 作用 | 说明 |
|------|------|------|
| `heicode/` | Manager 后端和默认前端 | Go + Gin/GORM,`web/default` 是当前网页端 |
| `cc-haha/` | Heicode 桌面客户端和本地服务 | Tauri + React + Bun,客户端到 Manager 的请求 body 会走加密/签名流程 |
| `docs/` | 项目文档、部署、集成、进度清单 | 后续交接优先看这里 |
| `docs/deployment/` | 生产部署和迁移文档 | VM、Azure、配置交接 |
| `docs/integration/` | Runtime、桌面客户端、蜂群、普通 sub 对接文档 | 联调时优先看 |
开发前先看:
| 范围 | 文档 |
|------|------|
| 根仓库规则 | `AGENTS.md` |
| Manager 规则 | `heicode/AGENTS.md` |
| 生产配置 | `docs/deployment/Heicode-Manager-生产配置与账号交接清单.md` |
| 普通 sub 桌面客户端对接 | `docs/integration/heicode-desktop-sub-agile-api.md` |
| 普通 sub Agent Manager 对接 | `docs/integration/普通sub敏捷模式-AgentManager对接任务清单.md` |
| 蜂群 Agent Manager 对接 | `docs/integration/蜂群模式-AgentManager对接任务清单.md` |
## 3. 生产拓扑
```text
用户/桌面客户端
-> https://code.xinghanlab.com
-> Heicode Manager Docker container on Azure VM
-> Azure PostgreSQL / Azure Redis
-> NewAPI model gateway
-> 普通 sub Agent Manager Runtime: http://20.212.121.126
-> 蜂群 Runtime / Orchestrator: http://52.139.240.116:8000
-> Azure Key Vault: https://heicode-kv.vault.azure.net
```
当前 Manager 部署在 Azure VM 上,容器名 `heicode`,端口 `3000:3000`。VM 上也能看到 `new-api`、`postgres`、`redis`、`heicode-openbao` 容器,但正式业务数据库和缓存应以 Azure 托管 PostgreSQL / Redis 配置为准,不要误用 VM 本地容器判断生产数据。
## 4. 普通 sub 和蜂群必须分开
这是最容易踩坑的点。
| 模式 | 含义 | Manager 当前配置 | Runtime |
|------|------|------------------|---------|
| 普通 sub 敏捷模式 | 桌面客户端把一个开发任务拆给若干子 Agent,按需求、设计、开发、测试、部署等阶段推进 | `AGENT_RUNTIME_ENABLED=true` | `http://20.212.121.126` |
| 蜂群模式 | HeiCode-Swarm 的多 Agent swarm run / task graph 模式 | `SWARM_RUNTIME_ENABLED=false` | `http://52.139.240.116:8000`,需单独启用和联调 |
不要因为两个接口都可能叫 `/api/swarms` 就把它们混成一个概念。普通 sub 是 Heicode 的任务组织方式;蜂群是独立 swarm runtime 形态。
## 5. 普通 sub 当前主流程
```text
桌面客户端创建/补充任务
-> Manager 用户态接口生成 deployment draft
-> Manager 调 Agent Manager Runtime POST /api/swarms
-> Runtime 创建 run 并启动子 Agent
-> Runtime 执行中 callback Manager
-> Manager 保存 timeline / events / logs / artifacts / usage
-> 桌面客户端从 Manager 查询展示
```
关键点:
1. Runtime 执行过程会回调状态、日志、timeline、用量。
2. 最终业务交付物不是直接塞在聊天文本里,而是通过 `artifact.created` 落库。
3. 客户端应先查 artifact 列表,再通过 content 接口下载完整产物。
4. 如果 `usage=0` 或只有失败摘要 artifact,不能算真实业务交付完成。
## 6. artifact 展示坑
桌面客户端曾出现“以下是作为 Frontend 角色...”这种内容,看起来像交付物,其实多半只是 Runtime 的摘要文本。
正常设计应该是:
| 层级 | 应展示什么 |
|------|------------|
| 聊天时间线 | 阶段进度、Agent 状态、摘要说明 |
| 交付产物卡片 | `artifact_id`、标题、类型、摘要、大小、hash、下载入口 |
| 完整代码/文件 | 通过 `GET /api/agent/user/deployments/{deployment_id}/artifacts/{artifact_id}/content` 下载 |
如果 artifact 里有 `azblob://...` 或 `runtime://...`,客户端不应该直接暴露云凭据或要求用户自己访问 Blob,而应通过 Manager / Runtime content 代理接口拿完整内容。
## 7. NewAPI / 模型调用边界
NewAPI 是模型网关和用量计费入口,不是 Heicode Manager 自己的模型执行器。
排查模型问题时要区分三条链路:
| 链路 | 调用方 | 常见问题 |
|------|--------|----------|
| 桌面普通聊天 | 桌面客户端 -> Manager/NewAPI | 用户 token、模型列表、渠道权限、body 加密 |
| 普通 sub Runtime Agent | Runtime 子 Agent -> NewAPI/Manager 模型网关 | Runtime 环境变量、模型名、base_url、请求路径、上游超时 |
| Manager 后台模型配置 | 管理员网页 -> Manager/NewAPI | 渠道配置、分组、可用模型、价格表达式 |
曾踩过的坑:
- 某个模型 503/504 时,不一定是客户端参数错,也可能是 Runtime 子 Agent 使用的模型、base_url 或请求格式不对。
- Runtime 需要回传 `newapi_request_id` 和非零 token usage,方便定位 NewAPI 日志。
- 一个模型失败时可以切换模型验证,但不能把失败摘要 artifact 当成业务完成。
## 8. Azure Key Vault / OpenBao 边界
当前正式方向是 Azure Key Vault,不是 OpenBao。
| 项目 | 结论 |
|------|------|
| Azure Key Vault | 正式长期密钥托管方案 |
| OpenBao | VM 上存在历史/兼容容器,不作为当前正式方案 |
| Managed Identity | Manager 访问 Key Vault 的推荐方式 |
| 当前已知问题 | 之前健康检查出现过 `Identity not found`,说明 VM 身份或 `AZURE_CLIENT_ID`/Vault 权限未配好 |
如果后续迁移到 Container Apps / AKS / App Service,不能只切域名。必须重新配置 Managed Identity、Key Vault 权限、环境变量、持久化、数据库/Redis 网络、Runtime callback 地址。
## 9. 登录、设备、模型列表问题排查
之前遇到过用户登录成功但设备看不到、模型列表拿不到的问题。排查顺序:
1. Manager 是否有该用户记录。
2. JWT/session 是否能通过 Manager 校验。
3. 设备注册/心跳是否入库。
4. 用户是否绑定 NewAPI channel/group/token。
5. NewAPI 返回是否 401/403/模型列表为空。
6. Mac/Windows 客户端请求是否走同一 base_url、同一加密/签名逻辑。
不要只看“客户端显示已登录”,已登录不代表模型、设备、NewAPI 绑定都完整。
## 10. 网页端 Manager 已做过的重点
已处理过的方向包括:
- 注册页国内邮箱提示。
- Manager 登录后菜单跳转问题。
- 任务总览 / deployments 页面部分英文文案中文化。
- 普通 sub 控制面、部署草稿、运行状态、timeline、events、logs、artifacts 展示。
- artifact content 获取链路文档。
- Azure Key Vault secret_ref 接入方向。
- PayPal 充值和 NewAPI 模型费用关系说明文档。
继续改网页端时必须真实点击验证,尤其是:
- 登录后左侧菜单。
- 模型、渠道、供应商、支付、部署、任务总览。
- 创建新运行弹窗/抽屉。
- 产物卡片和下载入口。
## 11. 部署和 git 规则
生产部署原则:
```text
本地修改
-> git commit
-> git push 到 heicode-mananger main
-> VM 上 git pull
-> docker compose ... up -d --build
-> sudo docker image prune -f
-> 真实接口/页面冒烟
```
注意:
- 不要用 `scp` 传整份源码到 VM。
- `HeiCode-issues.git` 已废弃,后续不用再更新。
- VM 构建后必须清理 Docker 镜像,避免磁盘被旧层占满。
- 不要提交 `.env`、密钥、token、数据库连接串。
## 12. 新 AI 接手建议顺序
1. 读 `AGENTS.md` 和 `heicode/AGENTS.md`。
2. 读 `docs/deployment/Heicode-Manager-生产配置与账号交接清单.md`。
3. 用 `git status` 确认是否有未提交变更,不要动无关文件。
4. 区分当前任务是普通 sub、蜂群、网页端、NewAPI、Azure 还是桌面客户端。
5. 先用接口确认真实状态,再下结论。
6. 涉及生产前先确认是否需要部署,部署后必须真实冒烟。
7. 涉及密码、token、连接串时只写配置名和获取位置,不写明文。
## 13. 已踩过的典型坑
| 坑 | 正确处理 |
|----|----------|
| 把普通 sub 和蜂群混在一起 | 两套模式、两套配置、两套联调清单 |
| artifact 摘要当完整交付 | 必须通过 content 接口拿完整产物 |
| Runtime 返回 completed 就算成功 | 还要看 usage、artifact、日志、是否失败摘要 |
| Key Vault health 报错只改代码 | 先查 Managed Identity 和 Vault 权限 |
| NewAPI 一个模型失败就判 Manager 错 | 查 Runtime 请求路径、模型名、request id、上游状态 |
| VM 上有 postgres/redis 容器就当生产库 | 以 `SQL_DSN`、`REDIS_CONN_STRING` 和 Azure 托管服务为准 |
| 修改网页后不点击验证 | 必须真实打开页面、点菜单、点按钮 |
| 把密钥写入 md 方便交接 | 只能写配置名、用途、位置,不能写明文 |
## 14. 当前后续重点
| 方向 | 后续任务 |
|------|----------|
| 普通 sub | 持续和桌面客户端联调真实开发任务,确认 artifact content 是完整业务产物 |
| 蜂群 | 如需启用,先配置 `SWARM_RUNTIME_*`,单独跑蜂群 E2E |
| Key Vault | 修复 Managed Identity / `AZURE_CLIENT_ID` / Key Vault 权限 |
| NewAPI | 保持 Runtime 回传 `newapi_request_id`、usage、成本信息 |
| 网页端 | 继续中文化、交互完善、真实点击测试 |
| 生产部署 | 每次部署后检查 `/api/status`、登录、deployments、Runtime health |
+2 -2
View File
@@ -11,7 +11,7 @@ Heicode 面向**多人协作、可追溯交付**的软件团队:把需求对
| `cc-haha/` | **Heicode**(终端与桌面客户端及本地服务;此为源码目录名)。 |
| `heicode/` | **Heicode Manager**(网关与管理控制台服务端;此为源码目录名)。 |
| `website/` | 产品介绍站点(Next.js;可 `pnpm dev` 或 Docker 预览)。 |
| `docs/` | 愿景与范式;**[`docs/milestones/`](./docs/milestones/README.md)** 交付里程碑;**[`docs/integration/`](./docs/integration/README.md)** Agnet 等平台接口设计。 |
| `docs/` | 愿景与范式;**[`docs/milestones/`](./docs/milestones/README.md)** 交付里程碑;**[`docs/integration/`](./docs/integration/README.md)** Agent 等平台接口设计。 |
## 产品在解决什么问题
@@ -68,7 +68,7 @@ cd website && pnpm install && pnpm dev
## 相关外部参考(概念)
- [oh-my-claudecode](https://ohmyclaudecode.com/) — Claude Code 类工具的高效实践参考。
- Agnet 平台以实际部署环境与文档为准。
- Agent 平台以实际部署环境与文档为准。
## 许可证
+16 -16
View File
@@ -7,8 +7,8 @@ Heicode 今日工作总结与后续计划
1. 项目主线梳理
- 已确认 docs/heicode.md 与 docs/plan.md 是当前实施主线。
- 已明确旧 Agnet API 草案、旧 M1-M5 计划、旧架构说明不作为当前实施依据。
- 已恢复 docs/README.md 为主线文档入口,并加入 Agnet 平台请求契约文档入口。
- 已明确旧 Agent API 草案、旧 M1-M5 计划、旧架构说明不作为当前实施依据。
- 已恢复 docs/README.md 为主线文档入口,并加入 Agent 平台请求契约文档入口。
2. P0 文档边界收敛
- 已完成 P0:文档入口与实施依据收敛。
@@ -21,15 +21,15 @@ Heicode 今日工作总结与后续计划
- 已加入 GORM 自动迁移。
- 新增认证 API:/api/resources 与 /api/resource-grants。
- 支持的资源类型包括:git、sk、project_document、cloud_account、cloud_resource。
- 资源授权可表达 tenant/project/resource 到 role/子 Agnet 的最小权限关系。
- 资源授权可表达 tenant/project/resource 到 role/子 Agent 的最小权限关系。
- 已实现 secret_ref-only 原则:只保存密钥引用,不保存明文密钥。
- 已加入校验,拒绝 metadata、permission_scope、constraints 中出现 password、token、secret、private_key、access_key、credential 等明文敏感字段。
4. Agnet 平台请求参数文档
- 已新增文档:docs/integration/agnet-platform-request-contract.md。
- 该文档说明 Manager 主动请求 Agnet 平台时需要携带的参数。
4. Agent 平台请求参数文档
- 已新增文档:docs/integration/agent-platform-request-contract.md。
- 该文档说明 Manager 主动请求 Agent 平台时需要携带的参数。
- 覆盖内容包括:
- 创建子 Agent 部署 POST /api/agnet/deployments。
- 创建子 Agent 部署 POST /api/agent/deployments。
- orchestration_plan、agents、sk_sources、runtime_execution、sk_access_policy、resource_grants 字段说明。
- secret_ref、安全边界、租户/项目边界、预算与模型约束。
- 部署状态查询、停止部署。
@@ -37,7 +37,7 @@ Heicode 今日工作总结与后续计划
- 监控接口:项目 dashboard snapshot、单部署 metrics。
- 事件接口、审计日志接口。
- SK 快照解析与查询接口。
- 已提交:ab71d5b docs: add agnet platform request contract。
- 已提交:ab71d5b docs: add agent platform request contract。
5. 团队协作与校验
- 已启动并完成 5 个 OMX worker 的协作任务。
@@ -80,7 +80,7 @@ Heicode 今日工作总结与后续计划
- 当前未执行生产 VM SSH 部署。
- 当前未对 Azure PostgreSQL、Redis、AKS 或其他云资源执行写操作。
- 当前未消耗可量化的线上运行资源。
- 本次新增的资源模型与 Agnet 请求契约为后续生产部署和平台联调准备。
- 本次新增的资源模型与 Agent 请求契约为后续生产部署和平台联调准备。
4. Git 资源
- 当前 main 相对 origin/main ahead 15 个提交。
@@ -100,7 +100,7 @@ Heicode 今日工作总结与后续计划
2. 当前可部署基础
- P0 文档边界已收敛。
- P1 Manager 资源模型最小后端能力已实现。
- Manager → Agnet 平台接口参数文档已补齐,包含日志和监控接口。
- Manager → Agent 平台接口参数文档已补齐,包含日志和监控接口。
- 变更包 controller/model/router 的测试与 vet 已通过。
3. 部署前阻塞/风险
@@ -124,21 +124,21 @@ Heicode 今日工作总结与后续计划
- 部署后检查服务健康、日志、API 可用性。
3. P1 后续完善
- 将 Resource Binding / Resource Grant 与现有项目、用户、Agnet 部署流更紧密打通。
- 将 Resource Binding / Resource Grant 与现有项目、用户、Agent 部署流更紧密打通。
- 完善资源授权 revoke/disable/status transition。
- 补充跨数据库兼容验证,尤其 PostgreSQL 线上迁移验证。
- 补充前端资源绑定与授权页面,避免展示任何 secret。
4. Agnet 平台联调
- 按 docs/integration/agnet-platform-request-contract.md 与 Agnet 平台确认最终字段。
4. Agent 平台联调
- 按 docs/integration/agent-platform-request-contract.md 与 Agent 平台确认最终字段。
- 联调创建部署、查询状态、停止部署、日志拉取、监控快照、事件、审计日志。
- 明确哪些接口由 Manager 内部提供,哪些接口由 Agnet 平台提供。
- 确认 Agnet 平台对日志和监控返回的脱敏策略。
- 明确哪些接口由 Manager 内部提供,哪些接口由 Agent 平台提供。
- 确认 Agent 平台对日志和监控返回的脱敏策略。
5. 测试与质量
- 解决或隔离 go test ./... 的既有失败。
- 确保 frontend/web 依赖可安装并完成 typecheck/lint/build。
- 增加 e2e 验证:创建资源 -> 创建授权 -> 创建 Agnet 部署 -> 查询事件/日志/监控。
- 增加 e2e 验证:创建资源 -> 创建授权 -> 创建 Agent 部署 -> 查询事件/日志/监控。
六、安全说明
@@ -0,0 +1,183 @@
# Heicode Manager 统一改造落地计划(Manager 侧)
> 版本:v0.1(初稿)
> 日期:2026-06-01
> 负责范围:仅 `heicode-mananger`(Manager 控制面 + `web/default` 前端)
> 性质:内部落地计划,随实现推进持续更新。不代表对外承诺。
> **状态更新(2026-06-01):`agnet → agent` 已按"强制全量、不留兼容"完成。**
> 后端 Go(路由 `/api/agent/*`、env `AGENT_*`、结构体/函数/文件名)、前端(`agent-console`/`agent-hub`、`/api/agent` 调用、i18n)、DB(表 `agent_*`、列 `agent_id`,并加幂等迁移 `renameAgnetTablesToAgent` 保住老数据)、文档、compose/.env、脚本均已统一为 `agent`。
> `go build ./...` 与 `go test ./controller ./model ./router ./middleware` 全绿;前端 `tsc -b` 通过。
> 本文 §3 原描述的"保留 `agnet` 兼容别名/线缆令牌"策略已**作废**——实际采用一次性硬切。唯一保留 `agnet` 字样的是 `model/main.go` 的迁移源表名(迁移所必需)。
> **部署待办**:① VM `.env` 的 `AGNET_*` 改名 `AGENT_*`;② 启动时迁移自动 rename 老表(已就绪);③ 其他三仓库(agent_management / HeiCode-Swarm / 客户端)需同步切到 `/api/agent` 与新 env,否则跨服务对接在它们切换前会中断(按你的要求强制先行)。
## 0. 基准文档
本计划是以下两份的 **Manager 侧拆解执行版**,结论以原文为准:
| 来源 | 位置 | 作用 |
|---|---|---|
| 统一调用方案 v0.1 | gitee `taijibaga/heicodedebug` → `2026-06-01_heicode客户端相关定义.md` | 四仓库总纲:接口归口、模式拆分、状态裁决、产物、回调 |
| 全链路代码评审报告 | gitee `taijibaga/heicodedebug` → `2026-06-01_Heicode全链路代码评审报告.md` | P1–P7 问题与证据 |
| 桌面 sub 对接文档 | `docs/integration/heicode-desktop-sub-agile-api.md` | 现行普通 sub 契约 |
四仓库分工(总纲 §4):客户端只调 Manager;**Manager 是唯一接口入口 + Runtime 路由器 + 唯一状态裁判**;`agent_management` 执行 Sub Agile;`HeiCode-Swarm` 执行 Swarm。
## 1. 贯穿全局的原则(来自总纲)
1. `agnet` 是历史拼写错误,统一为 `agent`;旧接口兼容期只做转发。
2. Sub Agile 与 Swarm 是两套运行时,**不再复用 `/api/swarms` 表达两种模式**,按 `mode` 路由。
3. Runtime 只上报**结构化事实**;Manager 据结构化字段裁决 `display_status`;客户端只消费结论,**不再用正则自判产物**。
4. 安全红线:只传 `azkv://` 等 `secret_ref`,禁明文密钥;高危操作 Manager 先审批再下发。
## 2. 已完成(分支 `fix/manager-deliverable-and-secret-validation`,commit `12602eb`,已推送)
| 项 | 文件 | 内容 | 对应总纲 |
|---|---|---|---|
| P2 交付物判定 | `controller/agnet_runtime_client.go` | `runtimeArtifactsAreSummaryOnly` 改读结构化 `artifact_type`+文件信号,弃脆弱正则 | §10.6 |
| P6a secret_ref 校验 | `controller/resource.go` | Resource CRUD 强制 `azkv://` | §16/§17.6 |
| P6b 值级密钥扫描 | `controller/resource.go`、`controller/agnet_control_plane.go` | `containsPlaintextSecret`/`containsSensitiveGrantField` 扫字符串值(sk-/JWT/PEM) | §16 |
| P5 默认模型收敛 | `controller/agnet_role_template.go`、`controller/agnet_task_bridge.go` | 单一来源 `defaultAgnetModelID()`(env `AGNET_DEFAULT_MODEL_ID`,默认 `gpt-5.4`);移除 `agnet-model-<role>` 占位回退 | §9 |
| P3 状态枚举 | `docs/integration/heicode-desktop-sub-agile-api.md` | 补 `completed` 终态 + runtime_state 镜像 + 未知值兜底 | §10.4 |
| 单测 | `controller/agnet_deliverable_secret_test.go` | 覆盖以上行为 | — |
> 注:`defaultAgnetModelID` 在后续命名统一中应改名 `defaultAgentModelID`,env 改 `AGENT_DEFAULT_MODEL_ID`(保留 `AGNET_*` 回退)。
## 3. 命名统一 `agnet → agent`
### 3.1 爆炸半径(仅本仓库,不含另三仓库与跨团队契约)
**2155 处 / 63 文件**(`rg -i agnet`)。按「客户端/其他服务是否依赖」分类:
| 类别 | 规模 | 对外可见 | 处理策略 | 风险 |
|---|---|---|---|---|
| HTTP 路由 `/api/agnet/*` | 路由表 | ✅ 客户端+Runtime | 新增 `/api/agent/*` 别名,旧保留转发 | 🔴 |
| JSON 字段 | 仅 `agnet_id`(3 处,含 DB 列) | ✅ resource grant 契约 | 响应 dual-emit `agent_id`,请求 dual-accept | 🟠 |
| env 变量 `AGNET_*` | compose + 代码读取 | ✅ 部署配置 | 代码先读 `AGENT_*` 回退 `AGNET_*` | 🟠 |
| DB 表 `agnet_*` / `agnet_id` 列 | ~6 表 | ❌ 内部 | **用 GORM `TableName()` 钉住物理名不动**,物理改名作为最后单独迁移 | 🔴 |
| Go 内部标识(`AgnetXxx`、函数、文件名 `agnet_*.go`) | ~1800 | ❌ 编译期可查 | 纯重构,分模块小步改 | 🟢 |
| 前端(`agnet-console/`、组件、i18n key) | ~250 | ❌ 内部(调的是路由) | 重构,随路由切换 | 🟢 |
| 文档 | 多处 | — | 新文档写 `agent`,`agnet` 标 deprecated | 🟢 |
### 3.2 兼容期规则(总纲 §19)
1. 旧接口只做转发。2. 新文档只写新接口。3. 新客户端只调新接口。4. Manager 内部存储统一 `agent` 命名。5. 日志可记 legacy route,不展示给普通用户。
## 4. 路由拆分与归口(总纲 §5)
### 4.1 客户端 → Manager(新增)
```
/api/heicode/capabilities
/api/heicode/sub-agile/tasks/... (或统一 /api/heicode/tasks + body.mode)
/api/heicode/swarm/tasks/...
```
### 4.2 Manager → Runtime(新增,按 mode 分流)
```
/api/agent/sub-agile/deployments -> agent_management (AGENT_RUNTIME_* / 现 AGNET_RUNTIME_*)
/api/agent/swarm/deployments -> HeiCode-Swarm (SWARM_RUNTIME_*)
```
Manager 已具备双 env 前缀分流机制(`agnetRuntimeClientConfigForMode`);蜂群侧配置就绪只差 `SWARM_RUNTIME_SERVICE_TOKEN`。
### 4.3 回调(新增 + 旧转发)
```
新:POST /api/agent/callbacks/runtime-events
旧:POST /api/agnet/callbacks/swarm-events (兼容转发到同一 handler)
```
## 5. 状态裁决:Manager 成为唯一裁判(总纲 §10,关联评审 P1)
### 5.1 三层状态 + display_status
- `client_task_status`(客户端本地)/ `cloud_deployment_status`(Manager)/ `runtime_execution_status`(Runtime)
- Manager 输出唯一 `display_status` 给客户端。
### 5.2 Runtime 必须上报的结构化交付物事实(契约)
```json
"deliverable": {
"has_deliverable": true,
"summary_only": false,
"artifact_ids": ["art_xxx"],
"files_modified": ["src/app.ts"],
"has_diff": true,
"commit_sha": ""
}
```
### 5.3 Manager 裁决规则
| Runtime 事实 | display_status |
|---|---|
| `completed` + has_deliverable + !summary_only | `completed` |
| `completed` + summary_only / 无 artifact | `completed_without_deliverable` 或 `needs_codegen` |
| `failed` | `failed` / `stopped` / `waiting_approval` 同名透传 |
### 5.4 Manager 落点
- `controller/agnet_callback.go`(`applyAgnetCallbackDeploymentState` 消费 `deliverable`,算 verdict)
- `model/agnet_deployment.go`(加 `DeliveryVerdict` / display_status 列,三库兼容 `varchar`)
- `controller/agnet_control_plane.go`(detail/timeline 暴露字段)
- 复用已就绪的 `artifactIsSummaryOnly`(P2)+ usage
- **阶段化**:先加派生字段不改 `status`(向后兼容)→ 客户端跟进后再引入新终态值。
## 6. 模型策略(总纲 §9)
- Sub Agile:`per_role` / `default`;Swarm:`primary`。
- 单一来源默认模型(P5 已起步,待改名 `defaultAgentModelID`)。
- Manager 校验:模型存在 / 套餐允许 / 角色允许 / 预算 / Runtime 支持。
## 7. 项目文件夹产物 `project_folder`(总纲 §12)—— 中期大件
- artifact 升为两级:`Project Artifact` → `File Artifacts / Directory Entries`。
- 新 artifact_type:`project_folder` / `project_archive`(现有 `code_patch/document/...` 保留)。
- 新增接口:`.../artifacts/{id}/manifest`、`.../files/{path}`、`.../archive`。
- **本地修改回传 + revision 协议**(§12.7):`local-edits` / `batch` / 冲突 `ARTIFACT_REVISION_CONFLICT` / Manager 维护 current accepted revision。
- 影响:新 model(artifact revision / project entries)、新 controller、content 代理扩展。**工程量大,单独立项。**
## 8. 云部署生命周期(总纲 §18)—— 远期
`project_folder` → 选 target(Azure/阿里云/AWS) → Manager 校验/审批/凭证/预算 → Deploy Worker/Runtime 执行 → `deployment_manifest` artifact 回传。**远期,本轮不展开。**
## 9. 安全与审计(总纲 §16/§17)
| 项 | 现状 | 待办 |
|---|---|---|
| `secret_ref` 强制 `azkv://` | ✅ agnet 路径 + Resource CRUD(P6a) | 覆盖其余写入路径 |
| 值级密钥扫描 | ✅ P6b | — |
| 高危操作 Manager gate | ❌ 现为立即 `accepted`,阻断依赖 Runtime | Manager 侧加 risk_level/pending 审批 gate(关联 P1/§17.1) |
| 短期凭证 lease 真派生 | ❌ 占位(只发 `lease://`,不 mint) | 接 Key Vault 派生短期凭证 |
| revoke 同步 Runtime | ⚠️ approve 有 sync,revoke 无 | 补 revoke→Runtime 通知 |
| 审计 fail-closed | ⚠️ best-effort | 关键审计改 fail-closed 或告警 |
| 客户端禁 inline secret_ref | — | Manager 改为接受 `resource_binding_id`,内部映射 secret_ref(§17.6) |
## 10. 落地阶段(总纲 §20,Manager 承担)
| 阶段 | Manager 任务 | 自主性 | 依赖 | 状态 |
|---|---|---|---|---|
| **1 接口/命名** | `/api/agent/*` 别名、`/api/heicode/{sub-agile,swarm}/*`、callback 新路由、`AGENT_*` env 回退、`agnet_id`→`agent_id` 双字段 | ✅ 加法自主 | — | 待开始 |
| **2 模式路由** | 按 `mode` 路由两套 `*_RUNTIME_*` | ✅ | 蜂群 token | 机制已就绪 |
| **3 模型策略** | per_role/primary 校验 | ✅ | — | P5 起步 |
| **4 状态/产物裁决** | display_status + deliverable 字段 + project_folder + revision | 🟡 | Runtime 发结构化事实 | P1/P2 起步 |
| **5 日志/回调/调试** | user_logs/debug_logs 分层 + diagnostics + 统一 callback | ✅ | Runtime callback 切换 | diagnostics 已有雏形 |
## 11. 跨团队依赖(Manager 做不了,需协调)
1. **agent_management / HeiCode-Swarm**:上报结构化 `deliverable` 事实;接入 `/api/agent/{sub-agile,swarm}/*` 路由;callback 切 `/api/agent/callbacks/runtime-events`;产出真实 `project_folder`。
2. **蜂群 Runtime**:`SWARM_RUNTIME_SERVICE_TOKEN` 安全配置 + 修复 single-agent fallback(评审 P7)。
3. **客户端(macOS/Windows)**:切新接口、消费 display_status、project 文件树展示、本地 edit 上传、禁 mock/直连(§17)。
## 12. 风险与红线
- **`agnet` 是 load-bearing**(路由/env/DB/跨服务契约)——**严禁全局 sed**,必须加法别名 + 兼容期。
- DB 物理表/列名暂不改,用 `TableName()` 钉住,避免迁移风险。
- 跨服务契约字段改动一律 **dual-emit + dual-accept**,且需与调用方协调切换节奏。
- 状态裁决先加字段不覆盖 `status`,避免误伤「真完成但 Runtime 未回 artifact」的任务。
## 13. 待确认事项
1. 统一方案仍是 **v0.1**,落地前需与作者对齐版本/范围。
2. approval decision 回传路径:`/api/agent/{mode}/deployments/{id}/approvals/{approval_id}` 的最终形态。
3. 客户端→Manager 路由用「分模式」还是「统一 `/api/heicode/tasks` + body.mode」。
4. `needs_codegen` 与 `completed_without_deliverable` 的判定边界(何时用哪个)。
5. project_folder / 云部署的优先级与排期(是否本阶段做)。
---
*维护:本文件随实现进度更新;每完成一项在「已完成/状态」列标注 commit。*
+4 -4
View File
@@ -10,9 +10,9 @@
| [`heicode-manager-sub-swarm-progress-checklist.md`](./heicode-manager-sub-swarm-progress-checklist.md) | Heicode Manager sub 模式、瀑布/敏捷、蜂群模式的已完成/未完成/依赖/风险/下一步进度清单 |
| [`heicode-manager-standalone-execution-plan.md`](./heicode-manager-standalone-execution-plan.md) | Heicode Manager 端可独立完成任务的执行计划、顺序、验收标准和边界 |
| [`integration/Heicode-登录接口对接文档.md`](./integration/Heicode-登录接口对接文档.md) | 已上线登录接口对接文档 |
| [`integration/agnet-platform-request-contract.md`](./integration/agnet-platform-request-contract.md) | Manager 请求 Agnet 平台时携带的部署、日志、监控、事件与审计接口参数 |
| [`deployment/azure-production-deploy-guardrails.md`](./deployment/azure-production-deploy-guardrails.md) | Azure VM / PostgreSQL / Redis / Agnet / NewAPI 生产部署前的安全守卫、环境变量注入和验证计划 |
| [`integration/agent-platform-request-contract.md`](./integration/agent-platform-request-contract.md) | Manager 请求 Agent 平台时携带的部署、日志、监控、事件与审计接口参数 |
| [`deployment/azure-production-deploy-guardrails.md`](./deployment/azure-production-deploy-guardrails.md) | Azure VM / PostgreSQL / Redis / Agent / NewAPI 生产部署前的安全守卫、环境变量注入和验证计划 |
旧 Agnet API 草案、旧里程碑、旧架构说明和旧上手材料不再作为实施依据。后续文档和实现以 `heicode.md` 与 `plan.md` 为准;生产部署操作以安全守卫文档约束,且不得覆盖产品/架构主线。
旧 Agent API 草案、旧里程碑、旧架构说明和旧上手材料不再作为实施依据。后续文档和实现以 `heicode.md` 与 `plan.md` 为准;生产部署操作以安全守卫文档约束,且不得覆盖产品/架构主线。
代码中的过渡期命名、旧接口注释或旧 UI 文案只作为现状参考;若与 `heicode.md` / `plan.md` 冲突,应先更新实现或另行补充当前主线文档,不得恢复旧 Agnet/M1-M5 草案作为依据。
代码中的过渡期命名、旧接口注释或旧 UI 文案只作为现状参考;若与 `heicode.md` / `plan.md` 冲突,应先更新实现或另行补充当前主线文档,不得恢复旧 Agent/M1-M5 草案作为依据。
@@ -43,22 +43,22 @@ HEICODE_PUBLIC_BASE_URL=https://code.xinghanlab.com
### 2.2 Agent Manager / 普通 sub / 蜂群联调配置
```env
AGNET_RUNTIME_ENABLED=true
AGNET_RUNTIME_BASE_URL=http://20.212.121.126
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_HEALTH_PATH=/api/agnet/health
AGNET_RUNTIME_TIMEOUT_SECONDS=15
AGNET_RUNTIME_SERVICE_TOKEN=<生产 service token>
AGNET_CALLBACK_TOKEN=<生产 callback 兼容 token>
AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF=azkv://heicode-kv.vault.azure.net/secrets/agnet-callback-signing-key
AGENT_RUNTIME_ENABLED=true
AGENT_RUNTIME_BASE_URL=http://20.212.121.126
AGENT_RUNTIME_CREATE_PATH=/api/swarms
AGENT_RUNTIME_STOP_PATH=/api/swarms/{swarm_id}/stop
AGENT_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id}
AGENT_RUNTIME_HEALTH_PATH=/api/agent/health
AGENT_RUNTIME_TIMEOUT_SECONDS=15
AGENT_RUNTIME_SERVICE_TOKEN=<生产 service token>
AGENT_CALLBACK_TOKEN=<生产 callback 兼容 token>
AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF=azkv://heicode-kv.vault.azure.net/secrets/agent-callback-signing-key
```
当前生产为了让 Agent Manager 自动 callback 先跑通,配置了 HMAC fallback:
```env
AGNET_CALLBACK_SIGNING_SECRET=<生产 callback 签名密钥>
AGENT_CALLBACK_SIGNING_SECRET=<生产 callback 签名密钥>
```
正式方案建议用 Azure Key Vault 托管该签名密钥,减少明文环境变量。
@@ -108,7 +108,7 @@ Key Vault:`heicode-kv`
Key Vault 里必须存在:
```text
agnet-callback-signing-key
agent-callback-signing-key
```
建议 Secret value 为 JSON:
@@ -153,7 +153,7 @@ agnet-callback-signing-key
- Redis 防火墙/VNet 是否允许新服务访问。
- Key Vault 防火墙是否允许新服务访问。
- 如果使用 VNet 集成,DNS 解析是否正常。
- Agent Manager 使用 IP 联调时,后续切域名要同步更新 `AGNET_RUNTIME_BASE_URL`。
- Agent Manager 使用 IP 联调时,后续切域名要同步更新 `AGENT_RUNTIME_BASE_URL`。
## 6. 域名和回调地址
@@ -176,7 +176,7 @@ HEICODE_PUBLIC_BASE_URL=https://code.xinghanlab.com
因为 Manager 发给 Agent Manager 的 callback 地址会基于它生成:
```text
https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
https://code.xinghanlab.com/api/agent/callbacks/swarm-events
```
如果临时域名联调,要么设置临时 `HEICODE_PUBLIC_BASE_URL`,要么确保正式域名已经能路由到新服务。
@@ -219,7 +219,7 @@ curl -fsS https://code.xinghanlab.com/api/status
使用测试用户登录后验证:
- 登录成功。
- `/api/agnet/user/deployments` 返回正常。
- `/api/agent/user/deployments` 返回正常。
- 页面 `https://code.xinghanlab.com/deployments` 可打开。
### 8.3 Key Vault 健康
@@ -1,8 +1,8 @@
# Azure 生产部署安全守卫与验证计划
本文用于 Heicode Manager / Agnet / NewAPI 相关生产发布前的人工执行检查。它只描述安全命令、环境变量名和验证项,不保存任何真实地址、账号、密码、Token、连接串、SSH key 或云访问密钥。
本文用于 Heicode Manager / Agent / NewAPI 相关生产发布前的人工执行检查。它只描述安全命令、环境变量名和验证项,不保存任何真实地址、账号、密码、Token、连接串、SSH key 或云访问密钥。
适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、Azure Key Vault 密钥保管、Agnet 平台联调、NewAPI 网关能力验证。
适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、Azure Key Vault 密钥保管、Agent 平台联调、NewAPI 网关能力验证。
## 1. 执行原则
@@ -11,8 +11,8 @@
| 凭据处理 | 只使用 VM 环境、交互式 SSH、未提交的 `.env`、Key Vault 或 `secret_ref`;禁止把真实密钥写入 Git、Markdown、终端报告或 team state。 |
| Git 发布 | 仅允许快进同步已审核提交;禁止在生产 VM 上提交代码或保存临时补丁。 |
| 数据库/Redis | Azure PostgreSQL / Redis 连接串只写入 VM 本地 `.env` 或 Secret Store;验证时只打印变量名和连通性结果,不打印值。 |
| Agnet / NewAPI | Manager 只传 `secret_ref`、部署计划、资源授权和审计上下文;禁止把明文云账号、数据库密码、模型 Key 放入请求体。 |
| Azure Key Vault | 只允许 Manager/Agnet 通过 Managed Identity 和受控网络访问;如果 Manager 提供客户端验证和绑定接口,Key Vault 不暴露普通公网访问。 |
| Agent / NewAPI | Manager 只传 `secret_ref`、部署计划、资源授权和审计上下文;禁止把明文云账号、数据库密码、模型 Key 放入请求体。 |
| Azure Key Vault | 只允许 Manager/Agent 通过 Managed Identity 和受控网络访问;如果 Manager 提供客户端验证和绑定接口,Key Vault 不暴露普通公网访问。 |
| 生产动作 | 执行 `up -d`、迁移、重启、回滚前必须记录当前镜像/提交和健康检查 URL;失败时停止扩大变更。 |
## 2. 本地发布前检查
@@ -97,7 +97,7 @@ ssh "$REMOTE" "cd '$REMOTE_DIR/heicode' && docker compose -f docker-compose.azur
- 对外域名统一使用 `heicode.xinghanlab.com`。
- Nginx 负责按路由转发 Manager 与 NewAPI,例如 Manager 主站、NewAPI 受控 API 或健康检查路由。
- Azure Key Vault 仅供 Manager/Agnet 服务端通过 Managed Identity 访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。
- Azure Key Vault 仅供 Manager/Agent 服务端通过 Managed Identity 访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。
- Key Vault 应优先使用 Private Endpoint;如临时启用公网访问,必须限制来源并在验证后关闭。
构建和启动:
@@ -122,9 +122,9 @@ VM_HOST="$VM_HOST" VM_USER="$VM_USER" REMOTE_DIR="$REMOTE_DIR/heicode" \
./heicode/bin/azure_vm_deploy.sh
```
## 7. Agnet 平台联调守卫
## 7. Agent 平台联调守卫
Manager 请求 Agnet 平台时遵循 `docs/integration/agnet-platform-request-contract.md`:
Manager 请求 Agent 平台时遵循 `docs/integration/agent-platform-request-contract.md`:
- `resource_grants[].secret_ref` 必须是 Secret Store 引用,不能是明文密钥。
- `repo_url` 不能包含用户名、密码或 Token。
@@ -134,22 +134,22 @@ Manager 请求 Agnet 平台时遵循 `docs/integration/agnet-platform-request-co
安全 smoke request 模板:
```bash
export AGNET_BASE_URL='<agnet-platform-base-url>'
export AGENT_BASE_URL='<agent-platform-base-url>'
export MANAGER_SERVICE_TOKEN_SECRET_REF='<secret-ref-only>'
export USER_ID='<manager-user-id>'
export BINDING_SCOPE='<resource-binding-scope>'
# 真实 token 由运行环境注入;禁止把 token 字面值写入命令历史或文档。
curl -fsS -X POST "$AGNET_BASE_URL/api/agnet/deployments" \
curl -fsS -X POST "$AGENT_BASE_URL/api/agent/deployments" \
-H 'Content-Type: application/json' \
-H "X-User-Id: $USER_ID" \
-H "X-Binding-Scope: $BINDING_SCOPE" \
-H "Idempotency-Key: deploy-$(date +%Y%m%d%H%M%S)" \
-H "Authorization: Bearer $MANAGER_SERVICE_TOKEN" \
--data @docs/integration/safe-agnet-deploy-example.json
--data @docs/integration/safe-agent-deploy-example.json
```
若没有 `safe-agnet-deploy-example.json`,先用本地临时文件生成并确认只包含 `secret_ref`,不要提交包含环境特定值的 payload。
若没有 `safe-agent-deploy-example.json`,先用本地临时文件生成并确认只包含 `secret_ref`,不要提交包含环境特定值的 payload。
## 8. 发布后验证清单
@@ -162,7 +162,7 @@ curl -fsS -X POST "$AGNET_BASE_URL/api/agnet/deployments" \
| Git 版本 | `git rev-parse --short HEAD` | 与已审核提交一致。 |
| DB/Redis 注入 | `awk -F= ... .env` | 只打印 key,包含 `SQL_DSN`、`REDIS_CONN_STRING`。 |
| 登录链路 | 调用登录文档中的生产验证流程 | 不在日志或报告输出 token。 |
| Agnet 部署 | 查询部署状态/事件/审计接口 | 能看到 deployment、events、audit,且无明文凭据。 |
| Agent 部署 | 查询部署状态/事件/审计接口 | 能看到 deployment、events、audit,且无明文凭据。 |
| NewAPI 网关能力 | 使用 Manager 受控模型调用或健康接口 | 只记录 request id、状态码、模型名,不记录 provider key。 |
## 9. 回滚与停止条件
@@ -172,7 +172,7 @@ curl -fsS -X POST "$AGNET_BASE_URL/api/agnet/deployments" \
1. 任一日志、响应、Markdown 或 Git diff 中出现真实密钥。
2. `git merge --ff-only` 失败或 VM 工作区有未知改动。
3. 健康检查连续失败。
4. Agnet 平台返回的事件/审计中包含明文凭据。
4. Agent 平台返回的事件/审计中包含明文凭据。
5. 数据库迁移或容器启动错误无法在一次重试内恢复。
回滚指针:
@@ -186,4 +186,4 @@ ssh "$REMOTE" "cd '$REMOTE_DIR/heicode' && IMAGE_TAG=\$(cat .last_success_image)
## 10. 本次文档产物状态
本文件仅准备部署守卫和验证计划;未执行 Git push、SSH 登录、Azure 资源变更、生产容器重启或 Agnet 实际部署。
本文件仅准备部署守卫和验证计划;未执行 Git push、SSH 登录、Azure 资源变更、生产容器重启或 Agent 实际部署。
@@ -6,7 +6,7 @@
## 一、核查结论
本文只列 Manager 端能独立完成的任务。判断标准是:不要求 Heicode 客户端新增功能、不要求蜂群 / Agnet Runtime 提供真实接口、不要求 AKS / NATS / Prometheus 等基础设施先上线。
本文只列 Manager 端能独立完成的任务。判断标准是:不要求 Heicode 客户端新增功能、不要求蜂群 / Agent Runtime 提供真实接口、不要求 AKS / NATS / Prometheus 等基础设施先上线。
| 结论 | 说明 |
|---|---|
@@ -20,12 +20,12 @@
| 事项 | 排除原因 | 需要谁配合 |
|---|---|---|
| 真实创建 Swarm Run 并返回 `swarm_id` | 需要蜂群平台提供 `POST /api/swarms` 或等价生产接口 | 蜂群 / Agnet Runtime |
| 子 Agnet claim、heartbeat、release、timeout | 需要 worker runtime 和任务池 | 蜂群 / Agnet Runtime |
| handoff、retry、blocked 的真实状态机 | 需要 Runtime 产生任务事件 | 蜂群 / Agnet Runtime |
| 真实创建 Swarm Run 并返回 `swarm_id` | 需要蜂群平台提供 `POST /api/swarms` 或等价生产接口 | 蜂群 / Agent Runtime |
| 子 Agent claim、heartbeat、release、timeout | 需要 worker runtime 和任务池 | 蜂群 / Agent Runtime |
| handoff、retry、blocked 的真实状态机 | 需要 Runtime 产生任务事件 | 蜂群 / Agent Runtime |
| 真实日志流和 CPU/内存/耗时指标 | 需要日志/指标源 | 蜂群 / 基础设施 |
| 客户端高危审批主弹窗 | 产品要求审批主体验在客户端 | Heicode 客户端 |
| 短期凭证真实注入子 Agnet | 需要受控 runtime、身份和网络通道 | 蜂群 / 基础设施 |
| 短期凭证真实注入子 Agent | 需要受控 runtime、身份和网络通道 | 蜂群 / 基础设施 |
| SK 工具真实调用结果 | 需要 SK 平台或 Runtime 上报 invocation event | 蜂群 / SK 平台 |
| 交付结果回到客户端 | 需要客户端展示和 Runtime artifact 输出 | 客户端 + 蜂群 |
@@ -34,9 +34,9 @@
| 顺序 | 任务 | 优先级 | 是否 Manager 独立 | 完成后价值 |
|---:|---|---|---|---|
| 1 | 增加 sub 模式字段 | P0 | 是 | 已完成:明确任务组织方式,支撑瀑布/敏捷状态展示 |
| 2 | 打通 HeicodeTask 到 Agnet deployment 的 Manager 桥接 | P0 | 是 | 已完成:任务可生成 deployment draft |
| 3 | 增加用户态 Agnet deployment API | P0 | 是 | 已完成:普通用户可以在自己资源范围内创建/查看/停止 deployment |
| 4 | 统一 `/api/swarms` 与 `/api/agnet/deployments` 边界 | P0 | 是 | 已完成:为后续蜂群联调留稳定 adapter |
| 2 | 打通 HeicodeTask 到 Agent deployment 的 Manager 桥接 | P0 | 是 | 已完成:任务可生成 deployment draft |
| 3 | 增加用户态 Agent deployment API | P0 | 是 | 已完成:普通用户可以在自己资源范围内创建/查看/停止 deployment |
| 4 | 统一 `/api/swarms` 与 `/api/agent/deployments` 边界 | P0 | 是 | 已完成:为后续蜂群联调留稳定 adapter |
| 5 | 建 callback 接收端骨架 | P0 | 是 | 已完成:先接住事件、artifact、审批请求、timeline |
| 6 | 建 artifact 数据模型和 API | P0 | 是 | 已完成:交付物/产物摘要落库并可展示 |
| 7 | 增加回调幂等和签名/服务身份校验骨架 | P1 | 是 | 已完成:重复回调不重复写入,支持 token/HMAC |
@@ -57,10 +57,10 @@
| 项 | 内容 |
|---|---|
| 目标 | Manager 能记录任务或部署计划采用 `waterfall` / `agile` 哪种组织方式 |
| 修改文件 | `heicode/model/agnet_deployment.go`、`heicode/controller/agnet_control_plane.go`、`heicode/web/default/src/features/agnet-console/api.ts`、`heicode/web/default/src/features/agnet-console/create-agnet-deployment-sheet.tsx` |
| 修改文件 | `heicode/model/agent_deployment.go`、`heicode/controller/agent_control_plane.go`、`heicode/web/default/src/features/agent-console/api.ts`、`heicode/web/default/src/features/agent-console/create-agent-deployment-sheet.tsx` |
| 建议字段 | `sub_mode`,枚举:`waterfall`、`agile`,默认 `agile` |
| 验收 | 创建 deployment 后 DB、API response、前端详情都能看到 `sub_mode` |
| 测试 | `go test ./controller -run 'TestAgnet.*SubMode|TestAgnetDeployment'`;`cd web/default && bun run typecheck` |
| 测试 | `go test ./controller -run 'TestAgent.*SubMode|TestAgentDeployment'`;`cd web/default && bun run typecheck` |
验收标准:
@@ -68,13 +68,13 @@
- 旧数据无字段时默认按 `agile` 展示。
- 不能把 `sub_mode` 当成蜂群 Runtime 流程,只表示 Heicode 任务组织方式。
### 任务 2:HeicodeTask 到 Agnet deployment 桥接
### 任务 2:HeicodeTask 到 Agent deployment 桥接
| 项 | 内容 |
|---|---|
| 目标 | Manager 能从 HeicodeTask 的任务卡生成本地 Agnet deployment payload |
| 修改文件 | `heicode/controller/agnet_control_plane.go`、新增 `heicode/controller/agnet_task_bridge.go`、`heicode/web/default/src/lib/heicode-mcp.ts`、`heicode/web/default/src/features/tasks/task-card-view.tsx` |
| 新增 API | `POST /api/agnet/tasks/:task_id/deployment-draft` 或等价 user-scoped endpoint |
| 目标 | Manager 能从 HeicodeTask 的任务卡生成本地 Agent deployment payload |
| 修改文件 | `heicode/controller/agent_control_plane.go`、新增 `heicode/controller/agent_task_bridge.go`、`heicode/web/default/src/lib/heicode-mcp.ts`、`heicode/web/default/src/features/tasks/task-card-view.tsx` |
| 新增 API | `POST /api/agent/tasks/:task_id/deployment-draft` 或等价 user-scoped endpoint |
| 输入 | task id、sub_mode、预算、资源范围、角色模板 |
| 输出 | deployment draft 或创建后的 `deployment_id` |
| 测试 | 新增 controller 单测;前端 typecheck |
@@ -85,14 +85,14 @@
- draft 中不能包含明文密钥,只能出现 `secret_ref`。
- 找不到 task 或资源授权不足时返回明确错误。
### 任务 3:用户态 Agnet deployment API
### 任务 3:用户态 Agent deployment API
| 项 | 内容 |
|---|---|
| 目标 | 普通用户可以创建、查询、停止自己资源范围内的 deployment |
| 修改文件 | `heicode/router/api-router.go`、`heicode/controller/agnet_control_plane.go`、`heicode/controller/agnet_control_plane_test.go` |
| 当前问题 | `/api/agnet/deployments` 走 `AdminAuth` |
| 新增建议 | 保留 admin route;新增 user route:`/api/agnet/user/deployments` 或在同一路由中按 user scope 限制 |
| 修改文件 | `heicode/router/api-router.go`、`heicode/controller/agent_control_plane.go`、`heicode/controller/agent_control_plane_test.go` |
| 当前问题 | `/api/agent/deployments` 走 `AdminAuth` |
| 新增建议 | 保留 admin route;新增 user route:`/api/agent/user/deployments` 或在同一路由中按 user scope 限制 |
| 测试 | 普通用户创建成功;越权查询别人 deployment 失败;停止别人 deployment 失败 |
验收标准:
@@ -102,13 +102,13 @@
- 请求体伪造别人 `user_context.user_id` 必须被覆盖或拒绝。
- resource grant 必须属于当前用户。
### 任务 4:统一 `/api/swarms` 与 `/api/agnet/deployments` 边界
### 任务 4:统一 `/api/swarms` 与 `/api/agent/deployments` 边界
| 项 | 内容 |
|---|---|
| 目标 | Manager 内部形成生产蜂群接口 adapter,不再让调用方混淆两个口径 |
| 修改文件 | 新增 `heicode/controller/agnet_swarm_adapter.go` 或 `heicode/service/agnet_swarm_adapter.go`,更新 `docs/integration/agnet-platform-request-contract.md` |
| 当前现实 | 本地已有 `/api/agnet/deployments`,蜂群资料包目标接口是 `/api/swarms` |
| 修改文件 | 新增 `heicode/controller/agent_swarm_adapter.go` 或 `heicode/service/agent_swarm_adapter.go`,更新 `docs/integration/agent-platform-request-contract.md` |
| 当前现实 | 本地已有 `/api/agent/deployments`,蜂群资料包目标接口是 `/api/swarms` |
| 独立做法 | 先实现 Manager 内部 adapter 和统一 DTO,真实外呼先留配置开关,默认走本地 control-plane |
| 测试 | adapter 单测验证 payload 字段、`secret_ref`、correlation_id、sub_mode |
@@ -123,8 +123,8 @@
| 项 | 内容 |
|---|---|
| 目标 | Manager 先具备接收蜂群平台回调的 API 和落库能力 |
| 修改文件 | `heicode/model/agnet_callback.go`、`heicode/controller/agnet_callback.go`、`heicode/router/api-router.go` |
| 新增接口 | `POST /api/agnet/callbacks/swarm-events` |
| 修改文件 | `heicode/model/agent_callback.go`、`heicode/controller/agent_callback.go`、`heicode/router/api-router.go` |
| 新增接口 | `POST /api/agent/callbacks/swarm-events` |
| 独立能力 | 本地模拟 payload 可保存、去重、查询;已补普通 sub 任务事件 schema 校验 |
| 测试 | controller 单测覆盖正常保存、重复 event_id 幂等、明文密钥拒绝、handoff 缺字段拒绝 |
@@ -140,7 +140,7 @@
| 项 | 内容 |
|---|---|
| 目标 | Manager 保存和展示 artifact 摘要,不等 Runtime 真实输出 |
| 修改文件 | 新增 `heicode/model/agnet_artifact.go`、`heicode/controller/agnet_artifact.go`、`heicode/web/default/src/features/agnet-console/api.ts`、`heicode/web/default/src/features/agnet-console/pages.tsx` |
| 修改文件 | 新增 `heicode/model/agent_artifact.go`、`heicode/controller/agent_artifact.go`、`heicode/web/default/src/features/agent-console/api.ts`、`heicode/web/default/src/features/agent-console/pages.tsx` |
| 字段 | `artifact_id`、`deployment_id`、`task_id`、`correlation_id`、`artifact_type`、`title`、`summary`、`uri`、`checksum`、`metadata_json`、`created_at` |
| 测试 | model/controller 单测;前端 typecheck |
@@ -155,8 +155,8 @@
| 项 | 内容 |
|---|---|
| 目标 | 生产联调前先有幂等和认证形状 |
| 修改文件 | `heicode/controller/agnet_callback.go`、新增 `heicode/middleware/agnet_callback_auth.go` |
| 机制 | `X-Request-Id`、`X-Correlation-Id`、`Idempotency-Key`、可选 `X-Agnet-Signature` |
| 修改文件 | `heicode/controller/agent_callback.go`、新增 `heicode/middleware/agent_callback_auth.go` |
| 机制 | `X-Request-Id`、`X-Correlation-Id`、`Idempotency-Key`、可选 `X-Agent-Signature` |
| 当前阶段 | 可以先用配置开关和本地测试 token,不接真实 Key Vault service token |
| 测试 | 缺少服务 token 时拒绝;重复 key 不重复写入 |
@@ -170,8 +170,8 @@
| 项 | 内容 |
|---|---|
| 目标 | 替换当前 `agnetSnapshots` 内存 map |
| 修改文件 | 新增 `heicode/model/agnet_sk_snapshot.go`,修改 `heicode/controller/agnet_control_plane.go` |
| 目标 | 替换当前 `agentSnapshots` 内存 map |
| 修改文件 | 新增 `heicode/model/agent_sk_snapshot.go`,修改 `heicode/controller/agent_control_plane.go` |
| 当前问题 | 容器重启后 `/sk-snapshots` 丢失 |
| 测试 | 创建 snapshot 后清空内存,再从 DB 查询仍存在 |
@@ -186,9 +186,9 @@
| 项 | 内容 |
|---|---|
| 目标 | 按 task/deployment/correlation_id 聚合 Manager 已有记录 |
| 修改文件 | `heicode/controller/agnet_control_plane.go`、`heicode/model/agnet_audit.go`、`heicode/web/default/src/features/agnet-console/pages.tsx` |
| 修改文件 | `heicode/controller/agent_control_plane.go`、`heicode/model/agent_audit.go`、`heicode/web/default/src/features/agent-console/pages.tsx` |
| 聚合内容 | deployment、audit events、approvals、leases、artifacts、callbacks、resource grants |
| 新增接口 | `GET /api/agnet/tasks/:task_id/timeline` 或 `GET /api/agnet/deployments/:id/timeline` |
| 新增接口 | `GET /api/agent/tasks/:task_id/timeline` 或 `GET /api/agent/deployments/:id/timeline` |
| 测试 | 同一 correlation_id 下能聚合多类事件 |
验收标准:
@@ -202,10 +202,10 @@
| 项 | 内容 |
|---|---|
| 目标 | 在蜂群 Runtime 未接入前,Manager 能用模拟事件自测完整链路 |
| 修改文件 | `heicode/controller/agnet_callback.go`、`heicode/router/api-router.go`、可选新增 `heicode/controller/agnet_smoke.go` |
| 接口建议 | admin-only `POST /api/agnet/dev/simulate-run` |
| 修改文件 | `heicode/controller/agent_callback.go`、`heicode/router/api-router.go`、可选新增 `heicode/controller/agent_smoke.go` |
| 接口建议 | admin-only `POST /api/agent/dev/simulate-run` |
| 生成内容 | deployment accepted、task.created、task.claimed、task.heartbeat、task.blocked、handoff.requested、handoff.completed、task.retried、task.completed、artifact.created、approval.requested、timeline.updated |
| 测试 | 单测验证模拟后 timeline/artifacts/audit 可查;`scripts/agnet_sub_mode_smoke.py` 可跑生产健康和 deployment timeline 冒烟 |
| 测试 | 单测验证模拟后 timeline/artifacts/audit 可查;`scripts/agent_sub_mode_smoke.py` 可跑生产健康和 deployment timeline 冒烟 |
验收标准:
@@ -219,7 +219,7 @@
| 项 | 内容 |
|---|---|
| 目标 | 用户能看到 Manager 自己可提供的闭环信息 |
| 修改文件 | `heicode/web/default/src/features/agnet-console/pages.tsx`、`api.ts`、必要时新增组件 |
| 修改文件 | `heicode/web/default/src/features/agent-console/pages.tsx`、`api.ts`、必要时新增组件 |
| 展示内容 | sub_mode、deployment 来源 task、callbacks、artifacts、timeline、SK snapshots 持久化状态 |
| 测试 | `cd heicode/web/default && bun run typecheck`;本地页面点击冒烟 |
@@ -263,7 +263,7 @@
|---|---|
| 目标 | 桌面客户端调用 Manager 的 sub 流程接口时,支持与模型调用一致的 V2 应用层 body 加密;浏览器后台页面继续兼容普通 HTTPS JSON |
| 修改文件 | `docs/integration/heicode-desktop-sub-agile-api.md`、`docs/heicode-runtime-auth-newapi-secret-design.md`、必要时补充 `heicode/middleware` 或客户端 SDK 加解密模块 |
| 当前现实 | `/api/agnet/user/*` 与 `/api/heicode-auth/*` 已支持 `Content-Encoding: heicode-aead-v1`;Manager 会先解密并校验设备签名,再把明文 JSON 交给业务处理或代理给上游 |
| 当前现实 | `/api/agent/user/*` 与 `/api/heicode-auth/*` 已支持 `Content-Encoding: heicode-aead-v1`;Manager 会先解密并校验设备签名,再把明文 JSON 交给业务处理或代理给上游 |
| 安全红线 | 请求体、回调、日志、artifact metadata、timeline 中禁止出现明文密码、Token、私钥、连接串、云 access key、模型 key;长期凭证只传 `secret_ref` |
| 后续判断 | 桌面客户端需要在 sub 流程调用中复用模型调用的 encryptedFetch/V2 签名能力;服务端保留未加密 JSON 只用于 Manager Web 控制台兼容 |
| 测试 | 单测覆盖 V2 malformed 请求拒绝、普通代理请求兼容;生产需用真实已配对设备发起加密 sub 请求冒烟 |
@@ -280,12 +280,12 @@
| 项 | 内容 |
|---|---|
| 目标 | Manager 不依赖客户端或 Runtime 改接口,也能把“callback 已到”和“Runtime/Agent 是否真的产出交付物”分开展示 |
| 修改文件 | `heicode/controller/agnet_runtime_client.go`、`heicode/router/api-router.go`、`heicode/web/default/src/features/agnet-console/api.ts`、`heicode/web/default/src/features/agnet-console/pages.tsx` |
| 新增 API | `GET /api/agnet/user/deployments/:deployment_id/runtime-diagnostics` |
| 诊断来源 | 用 deployment 记录里的 `runtime_swarm_id` / `runtime_deployment_id` 查询 Runtime status;默认路径 `/api/swarms/{swarm_id}/status`,可用 `AGNET_RUNTIME_STATUS_PATH` / `SWARM_RUNTIME_STATUS_PATH` 配置 |
| 修改文件 | `heicode/controller/agent_runtime_client.go`、`heicode/router/api-router.go`、`heicode/web/default/src/features/agent-console/api.ts`、`heicode/web/default/src/features/agent-console/pages.tsx` |
| 新增 API | `GET /api/agent/user/deployments/:deployment_id/runtime-diagnostics` |
| 诊断来源 | 用 deployment 记录里的 `runtime_swarm_id` / `runtime_deployment_id` 查询 Runtime status;默认路径 `/api/swarms/{swarm_id}/status`,可用 `AGENT_RUNTIME_STATUS_PATH` / `SWARM_RUNTIME_STATUS_PATH` 配置 |
| 普通 sub / 蜂群边界 | 返回 `runtime_mode`,页面分别显示“普通 sub 模式”或“蜂群模式”,不把二者合并成一个流程 |
| 已识别异常 | Runtime agent failed、completed 但存在 failed agents、只有 `Runtime execution summary` 兜底产物、模型 token 用量为 0、Runtime 状态查询失败 |
| 测试 | `go test ./controller -run TestAgnetRuntimeDiagnosticsWarnsOnCompletedRuntimeWithFailedAgents -count=1`;前端 `bun run build:check` |
| 测试 | `go test ./controller -run TestAgentRuntimeDiagnosticsWarnsOnCompletedRuntimeWithFailedAgents -count=1`;前端 `bun run build:check` |
验收标准:
@@ -299,11 +299,11 @@
| 项 | 内容 |
|---|---|
| 目标 | 对齐 Agent Manager v2.1.10 的产物获取流程:Manager / 前端先查 artifact 列表,再通过用户态 content 代理接口获取完整文件 |
| 修改文件 | `heicode/model/agnet_artifact.go`、`heicode/controller/agnet_callback.go`、`heicode/controller/agnet_runtime_client.go`、`heicode/router/api-router.go`、`heicode/web/default/src/features/agnet-console/pages.tsx` |
| 新增 API | `GET /api/agnet/user/deployments/:deployment_id/artifacts/:artifact_id/content` |
| Runtime 目标路径 | 默认 `GET /api/swarms/{swarm_id}/artifacts/{artifact_id}/content`,可用 `AGNET_RUNTIME_ARTIFACT_CONTENT_PATH` / `SWARM_RUNTIME_ARTIFACT_CONTENT_PATH` 配置 |
| 修改文件 | `heicode/model/agent_artifact.go`、`heicode/controller/agent_callback.go`、`heicode/controller/agent_runtime_client.go`、`heicode/router/api-router.go`、`heicode/web/default/src/features/agent-console/pages.tsx` |
| 新增 API | `GET /api/agent/user/deployments/:deployment_id/artifacts/:artifact_id/content` |
| Runtime 目标路径 | 默认 `GET /api/swarms/{swarm_id}/artifacts/{artifact_id}/content`,可用 `AGENT_RUNTIME_ARTIFACT_CONTENT_PATH` / `SWARM_RUNTIME_ARTIFACT_CONTENT_PATH` 配置 |
| 安全边界 | Manager 先校验当前用户拥有 deployment,再校验 artifact 属于该 deployment;不暴露 Azure Blob 凭据、SAS URL 或 Runtime 内网信息 |
| 测试 | `go test ./controller -run TestAgnetArtifactContentProxiesRuntimeContent -count=1` |
| 测试 | `go test ./controller -run TestAgentArtifactContentProxiesRuntimeContent -count=1` |
验收标准:
@@ -319,7 +319,7 @@
| 顺序 | 任务 |
|---:|---|
| 1 | sub 模式字段 |
| 2 | 用户态 Agnet deployment API |
| 2 | 用户态 Agent deployment API |
| 3 | HeicodeTask 到 deployment 桥接 |
| 4 | 本地模拟蜂群事件冒烟入口 |
@@ -329,7 +329,7 @@
| 顺序 | 任务 |
|---:|---|
| 1 | `/api/swarms` 与 `/api/agnet/deployments` adapter |
| 1 | `/api/swarms` 与 `/api/agent/deployments` adapter |
| 2 | callback 接收端骨架 |
| 3 | artifact 数据模型和 API |
| 4 | 回调幂等和服务身份校验骨架 |
@@ -10,14 +10,14 @@
|---|---|
| `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/product-package/07-integration-boundaries.md` | Heicode、Manager、Agent 平台、CodeGW、Azure Key Vault 的边界 |
| `docs/integration/heicode-desktop-sub-agile-api.md` | Heicode 桌面客户端接 Manager 的普通 sub 敏捷流程 |
| `docs/integration/Heicode-Manager-PayPal支付接入与计费关系说明.md` | PayPal 收款、Heicode 余额/订阅、NewAPI 模型扣费和 Agent 运行费用边界 |
| 当前仓库 `heicode/` 代码 | Manager 端实际实现核查 |
## 一、核心边界
蜂群模式不是普通 sub 敏捷/瀑布本身。普通 sub 是 Heicode 的任务组织方式;蜂群是 Agnet/Swarm Runtime 的执行方式。
蜂群模式不是普通 sub 敏捷/瀑布本身。普通 sub 是 Heicode 的任务组织方式;蜂群是 Agent/Swarm Runtime 的执行方式。
| 模式 | Manager 当前职责 | Runtime / Agent Manager 当前职责 | 不能混淆的点 |
|---|---|---|---|
@@ -28,7 +28,7 @@
|---|---|---|---|
| Heicode 桌面客户端 | 用户主体验 | 输入目标、持续补充需求、查看反馈、审批高危操作、接收交付结果 | 直接配置 AKS、模型供应商、完整蜂群 payload |
| Heicode Manager | 控制面、记录面和用户侧账本入口 | 资源绑定、`secret_ref`、权限清单、生成启动请求、记录 deployment/swarm 映射、回调、artifact、timeline、审批、审计、余额/订阅展示 | 替代客户端做主开发对话,替代 Runtime 执行任务,或把 PayPal 收款当成模型扣费链路 |
| HeiCode-Swarm / Agnet Runtime | 执行层 | 创建 Swarm Run、任务图、Agent 编队、claim、heartbeat、handoff、执行、结果回传、真实运行 usage 回传 | 保存长期明文密钥,直接暴露给普通用户,或自行决定用户账本扣费 |
| HeiCode-Swarm / Agent Runtime | 执行层 | 创建 Swarm Run、任务图、Agent 编队、claim、heartbeat、handoff、执行、结果回传、真实运行 usage 回传 | 保存长期明文密钥,直接暴露给普通用户,或自行决定用户账本扣费 |
## 二、目标调用链
@@ -38,7 +38,7 @@ Heicode 桌面客户端
- V2 加密请求 body
- task/deployment draft
- resource_grants / secret_ref / budget / approval_policy
-> Agnet Runtime 或 HeiCode-Swarm
-> Agent Runtime 或 HeiCode-Swarm
- POST /api/swarms 或兼容创建入口
- 返回 swarm_id / runtime_deployment_id
<- Runtime callback
@@ -54,24 +54,24 @@ Heicode 桌面客户端
| 能力 | 当前状态 | 代码证据 |
|---|---|---|
| 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` |
| Runtime 状态诊断 | 已新增用户态只读诊断接口,按 `runtime_mode` 区分普通 sub / 蜂群,查询 Runtime status 并识别 failed agent、兜底摘要 artifact、零 token 用量等异常 | `heicode/controller/agnet_runtime_client.go`、`AgnetGetUserDeploymentRuntimeDiagnostics` |
| 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` |
| artifact 完整内容代理 | 已新增用户态 content 下载接口,Manager 校验 deployment/artifact 权限后代理 Runtime content 接口读取完整产物 | `AgnetGetUserDeploymentArtifactContent`、`callAgnetRuntimeArtifactContent` |
| 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/*`、`/api/swarms` 已按同一套 V2 设备签名和 body 加密路径设计;未加密 Web 控制台仍兼容 session + `New-Api-User` | `heicode/middleware/auth.go`、`heicode/router/api-router.go` |
| sub 模式字段 | 已支持 `sub_mode`,默认 `agile`,校验 `agile/waterfall` | `heicode/controller/agent_control_plane.go`、`heicode/model/agent_deployment.go` |
| 用户态 deployment | 已有 `/api/agent/user/deployments` 创建、查询、停止、日志、事件、指标、artifact、SK snapshot、timeline | `heicode/router/api-router.go` |
| 任务到 deployment draft | 已有 `/api/agent/user/tasks/:task_id/deployment-draft` | `heicode/controller/agent_task_bridge.go` |
| `/api/swarms` 兼容入口 | 已有用户态 `POST /api/swarms`,内部走 Manager deployment 创建,并作为 adapter source 记录 | `heicode/router/api-router.go`、`AgentCreateUserSwarm` |
| Runtime 创建桥接 | 已能按配置调用 Runtime 创建接口,默认路径 `/api/agent/deployments`,可用环境变量改为蜂群创建路径 | `heicode/controller/agent_runtime_client.go` |
| Runtime stop 桥接 | 已能在停止 Manager deployment 时调用 Runtime stop | `heicode/controller/agent_runtime_client.go` |
| Runtime 状态诊断 | 已新增用户态只读诊断接口,按 `runtime_mode` 区分普通 sub / 蜂群,查询 Runtime status 并识别 failed agent、兜底摘要 artifact、零 token 用量等异常 | `heicode/controller/agent_runtime_client.go`、`AgentGetUserDeploymentRuntimeDiagnostics` |
| callback 接收 | 已有 `POST /api/agent/callbacks/swarm-events` | `heicode/controller/agent_callback.go` |
| callback 鉴权 | 支持 `X-Agent-Service-Token` 和 HMAC 签名校验,并可从 Key Vault ref 读取签名密钥 | `heicode/controller/agent_callback.go` |
| callback 幂等 | `event_id` / `idempotency_key` 去重,重复回调返回成功但不重复写 | `heicode/model/agent_callback.go` |
| artifact 落库 | `artifact.created` 可生成 artifact 记录,支持用户态列表查询 | `heicode/model/agent_artifact.go`、`AgentListUserDeploymentArtifacts` |
| artifact 完整内容代理 | 已新增用户态 content 下载接口,Manager 校验 deployment/artifact 权限后代理 Runtime content 接口读取完整产物 | `AgentGetUserDeploymentArtifactContent`、`callAgentRuntimeArtifactContent` |
| approval 回调 | `approval.requested` 可转成 Manager 审批记录 | `heicode/controller/agent_callback.go` |
| 审批结果回传 Runtime | 用户 approve/reject 后,Manager 可按配置 POST 回 Runtime approval decision,且不发送 `secret_ref` | `heicode/controller/agent_approval.go`、`heicode/controller/agent_runtime_client.go` |
| timeline 聚合 | 用户态 timeline 聚合 audit、callbacks、artifacts、sk_snapshots | `AgentGetUserDeploymentTimeline` |
| SK snapshot 持久化 | 已有 `agent_sk_snapshots` 模型和列表查询 | `heicode/model/agent_sk_snapshot.go` |
| 本地模拟事件 | 已有用户态 `simulate-events`;默认模拟会写入 callback、artifact、approval、timeline 记录,用于 Manager 自测展示链路和脱敏检查 | `AgentSimulateUserDeploymentEvents` |
| V2 body 加密 | `/api/agent/user/*`、`/api/heicode-auth/*`、`/api/swarms` 已按同一套 V2 设备签名和 body 加密路径设计;未加密 Web 控制台仍兼容 session + `New-Api-User` | `heicode/middleware/auth.go`、`heicode/router/api-router.go` |
| 生产普通 sub 烟测记录 | 2026-05-31 已用生产 Manager 入口完成真实普通 sub 复核:`dep_1d6d66896cc6` -> `swm_03995f7c7a27`,`gpt-5.4`,`tokens_used=2682`,`newapi_request_id=chatcmpl-DlbZce3VZnsv5DptBILFoiRYy5ZHN`,业务 `code_patch` artifact 可通过 Manager content 接口下载 | `docs/integration/heicode-desktop-sub-agile-api.md` |
| PayPal/计费边界文档 | 已明确 PayPal 只是收款渠道;模型调用仍走 Heicode/NewAPI 的钱包或订阅额度;Agent 运行费用目前只有预算字段,真实收费需 Runtime usage 回传 | `docs/integration/Heicode-Manager-PayPal支付接入与计费关系说明.md` |
@@ -81,8 +81,8 @@ Heicode 桌面客户端
|---|---|---|---|---|
| P0 | 把 `/api/swarms` adapter 文档化并固定字段 | 已完成:`docs/integration/蜂群模式-AgentManager对接任务清单.md` 和 `docs/integration/普通sub敏捷模式-AgentManager对接任务清单.md` 已写清 Runtime create、callback、source、`deployment_id <-> swarm_id` 映射 | 是 | 文档可直接发给 Agent Manager / 蜂群侧核对 |
| P0 | 增加 Swarm Run 显示字段 | 已完成:用户态 deployment 详情展示 `runtime_deployment_id`、`runtime_swarm_id`、`runtime_state`,并在相关记录里展示 source | 是 | 用户态 deployment 详情和后台页面能看到 Runtime 映射 |
| P0 | 回调事件类型收敛 | 已完成:`GET /api/agnet/callbacks/swarm-events/schema` 输出事件类型、分类和必填字段;callback 接收端按 schema 校验关键 task/handoff/artifact/approval 字段 | 是 | `task.created/claimed/running/completed/failed/handoff/approval/artifact` 都有 schema |
| P0 | Runtime 联调配置模板 | 已完成:两份 Agent Manager 对接任务清单已写清 `AGNET_RUNTIME_*`、callback URL、service token/HMAC 方式和验收步骤 | 是 | 蜂群项目按模板能调用 Manager callback |
| P0 | 回调事件类型收敛 | 已完成:`GET /api/agent/callbacks/swarm-events/schema` 输出事件类型、分类和必填字段;callback 接收端按 schema 校验关键 task/handoff/artifact/approval 字段 | 是 | `task.created/claimed/running/completed/failed/handoff/approval/artifact` 都有 schema |
| P0 | Runtime 联调配置模板 | 已完成:两份 Agent Manager 对接任务清单已写清 `AGENT_RUNTIME_*`、callback URL、service token/HMAC 方式和验收步骤 | 是 | 蜂群项目按模板能调用 Manager callback |
| P1 | 审批结果回传 Runtime 联调 | Manager adapter 已有;仍需要 Runtime 提供接收接口并验证状态继续/停止 | 需要 Runtime 接口 | 审批通过/拒绝后 Runtime 状态能继续或停止 |
| P1 | Artifact 展示优化 | Manager 端已完成:页面展示 artifact 类型、摘要和 URI;真实 `code_patch/document/test_report/deployment_manifest` 仍需 Runtime 输出 | 需要 Runtime 数据 | artifact 页面/详情能按类型展示摘要和链接 |
| P1 | Artifact 完整内容下载 | 已完成:用户态 `/artifacts/{artifact_id}/content` 代理 Runtime content,页面提供下载入口 | 是 | artifact 属于当前用户 deployment 才能下载,响应透传 Runtime 文件内容 |
@@ -91,8 +91,8 @@ Heicode 桌面客户端
| P1 | 日志/指标真实来源标识 | 已完成:logs/metrics API 返回 `data_source`、`runtime_source`,当前明确是 Manager control-plane / estimated,不伪装 Runtime 真实指标 | 需要 Runtime 数据 | 页面和 API 响应能区分来源 |
| P1 | Agent 运行费用口径收敛 | 已完成文档口径:`budget.max_tokens/max_cost_usd/max_duration_sec` 是预算约束,不等于真实扣费账本;真实收费必须依赖 Runtime 回传 usage | Manager 已完成文档,真实数据需 Runtime | 页面/文档不把 estimated budget 说成真实扣费 |
| P1 | 高危审批客户端联动文档 | 已完成:`docs/integration/heicode-desktop-sub-agile-api.md` 已包含 approval 查询、approve/reject、awaiting_approval 流程 | 是 | 客户端文档补齐 approval flow |
| P2 | 蜂群模式验收脚本 | 已完成:`scripts/agnet_sub_mode_smoke.py` 支持 schema 检查、生产健康检查、可选 simulate-events、可选真实 callback smoke | 是 | 本地/生产能跑出 callback、artifact、approval、timeline 可见 |
| P2 | 生产 schema / 认证链路复测 | 本地代码和测试已覆盖;生产公开 `GET /api/agnet/callbacks/swarm-events/schema` 当前返回 404,认证接口需有效登录态或后台 token 才能测 | 是,部署后复测 | 生产 schema 返回 200,用户态/后台态 smoke 能拿到真实数据 |
| P2 | 蜂群模式验收脚本 | 已完成:`scripts/agent_sub_mode_smoke.py` 支持 schema 检查、生产健康检查、可选 simulate-events、可选真实 callback smoke | 是 | 本地/生产能跑出 callback、artifact、approval、timeline 可见 |
| P2 | 生产 schema / 认证链路复测 | 本地代码和测试已覆盖;生产公开 `GET /api/agent/callbacks/swarm-events/schema` 当前返回 404,认证接口需有效登录态或后台 token 才能测 | 是,部署后复测 | 生产 schema 返回 200,用户态/后台态 smoke 能拿到真实数据 |
## 五、需要蜂群项目配合的事项
+2 -2
View File
@@ -17,7 +17,7 @@ Manager 端已经具备蜂群联调需要的控制面基础:用户态 deployme
| 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 持续回调 | 蜂群 |
| 接收 `swarm-events` 回调 | 已有 `POST /api/agent/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 回调真实数据 | 蜂群 |
@@ -85,7 +85,7 @@ desktop-client -> Orchestrator(FastAPI) -> Redis -> Agent Pods
| 缺失项 | 当前状态 | 处理建议 |
|---|---|---|
| 蜂群验收脚本 | 已新增 `scripts/agnet_sub_mode_smoke.py`,可检查生产 Manager、Agent health 和指定 deployment timeline | 后续按真实 Runtime deployment 固化执行参数 |
| 蜂群验收脚本 | 已新增 `scripts/agent_sub_mode_smoke.py`,可检查生产 Manager、Agent health 和指定 deployment timeline | 后续按真实 Runtime deployment 固化执行参数 |
| 页面术语统一 | sub/蜂群容易混淆 | 页面和文档统一:sub 是任务组织,swarm 是执行层 |
| 兼容 `HeiCode-Swarm` demo client 的说明 | 容易误认为正式 Heicode 桌面客户端 | 文档明确 demo client 不等于 `cc-haha` 正式客户端 |
@@ -2,26 +2,26 @@
日期:2026-05-04
本文修正 Manager、Agnet 平台、NewAPI 与 Azure Key Vault 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。
本文修正 Manager、Agent 平台、NewAPI 与 Azure Key Vault 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。
## 一、用户输入在哪里
用户输入发生在 Heicode 的用户侧入口,也就是 Manager/客户端的“想法输入”主流程。
用户登录后输入目标、需求、约束、绑定的 Git/SK/云资源选择,以及是否批准高危操作。Manager 负责把这些输入整理成 Agnet 平台可执行的 work request:
用户登录后输入目标、需求、约束、绑定的 Git/SK/云资源选择,以及是否批准高危操作。Manager 负责把这些输入整理成 Agent 平台可执行的 work request:
- 用户想法和自然语言需求。
- 绑定的 Git 仓库、分支、路径范围和写入权限。
- 绑定的 SK 仓库或技能包。
- 绑定的云资源元数据和允许动作。
- 子 Agnet 角色、数量、运行模型和预算限制。
- 子 Agent 角色、数量、运行模型和预算限制。
- 客户端已经确认的高危操作审批结果。
Manager 不应为了团队开发控制额外发明 `tenant/project` 产品概念。当前团队开发边界优先来自绑定的 Git 仓库、允许路径、分支策略、Agnet 角色和资源授权。
Manager 不应为了团队开发控制额外发明 `tenant/project` 产品概念。当前团队开发边界优先来自绑定的 Git 仓库、允许路径、分支策略、Agent 角色和资源授权。
## 二、Manager 用户复用 Agnet 登录体系
## 二、Manager 用户复用 Agent 登录体系
Manager User 应复用 Heicode/Agnet 已上线登录体系,不再另建一套独立身份。
Manager User 应复用 Heicode/Agent 已上线登录体系,不再另建一套独立身份。
参考 [`integration/Heicode-登录接口对接文档.md`](./integration/Heicode-登录接口对接文档.md),登录流程已经提供:
@@ -51,7 +51,7 @@ JWT 中也包含:
因此 Manager 的认证设计应是:
```text
用户登录 Heicode/Agnet 认证体系
用户登录 Heicode/Agent 认证体系
-> Manager 校验 token 并读取 /api/auth/me
-> Manager 使用 user.id / sub 作为业务用户 ID
-> Manager 使用 channelId 关联 NewAPI 余额、用量和扣费查询
@@ -62,7 +62,7 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
## 三、高危操作审批与 Azure Key Vault 密钥注入
高危操作审批只在客户端完成。用户在客户端明确批准后,Manager/Agnet 平台才可以执行对应动作。Agnet 平台不是审批主体,不发起额外审批;它只校验 `approval_id`、审批主体、审批范围、TTL、`risk_level` 和策略是否匹配。
高危操作审批只在客户端完成。用户在客户端明确批准后,Manager/Agent 平台才可以执行对应动作。Agent 平台不是审批主体,不发起额外审批;它只校验 `approval_id`、审批主体、审批范围、TTL、`risk_level` 和策略是否匹配。
密钥处理边界如下:
@@ -71,16 +71,16 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
-> 用户授权或绑定资源
-> Manager 通过 VM Managed Identity 写入 Azure Key Vault
-> Manager DB 只保存 secret_ref
-> Azure Key Vault 通过 Private Endpoint / 防火墙限制,只允许 Manager/Agnet 受控网络访问
-> Azure Key Vault 通过 Private Endpoint / 防火墙限制,只允许 Manager/Agent 受控网络访问
高危操作
-> 客户端审批
-> Manager/Agnet 平台按 secret_ref 从 Azure Key Vault 获取或派生短期凭证
-> 短期凭证可注入子 Agnet
-> 子 Agnet 完成任务后凭证过期或撤销
-> Manager/Agent 平台按 secret_ref 从 Azure Key Vault 获取或派生短期凭证
-> 短期凭证可注入子 Agent
-> 子 Agent 完成任务后凭证过期或撤销
```
允许注入子 Agnet 的只能是短期、最小权限、可审计的凭证。长期 Git token、云 access key、SSH 私钥、数据库密码、NewAPI key 原文不得进入 Git、Markdown、普通日志或长期 Agnet 状态。
允许注入子 Agent 的只能是短期、最小权限、可审计的凭证。长期 Git token、云 access key、SSH 私钥、数据库密码、NewAPI key 原文不得进入 Git、Markdown、普通日志或长期 Agent 状态。
公网入口边界:
@@ -93,7 +93,7 @@ Azure Key Vault 不暴露普通公网入口的检查口径:
- Key Vault 应优先关闭 Public Network Access,并通过 Private Endpoint 接入 `heicode-vnet`。
- 外部客户端只能通过 Manager 的认证、资源绑定、审批和 `secret_ref` 管理接口间接操作密钥引用。
- Manager、Agnet 平台和子 Agnet 访问 Azure Key Vault 时必须走 Private Endpoint / Workload Identity / Managed Identity 绑定。
- Manager、Agent 平台和子 Agent 访问 Azure Key Vault 时必须走 Private Endpoint / Workload Identity / Managed Identity 绑定。
- 健康检查和联调报告只能证明受控网络访问可用;不得把公网可访问作为验收口径。
短期凭证注入必须满足:
@@ -102,7 +102,7 @@ Azure Key Vault 不暴露普通公网入口的检查口径:
- 有 `secret_ref` 来源。
- 有 TTL 或明确撤销机制。
- 有资源范围、路径范围、云资源范围或 API 范围限制。
- 有 Agnet deployment、agent role、user id、操作类型的审计记录。
- 有 Agent deployment、agent role、user id、操作类型的审计记录。
## 四、NewAPI 额度与扣费能力
@@ -125,22 +125,22 @@ NewAPI 官方 skill 文档明确支持:
3. Manager 普通用户侧展示应围绕当前登录用户的模型可用性、余额、额度、调用量和调用日志。
4. 扣费映射应优先使用 `channelId`、NewAPI user、NewAPI token 或 NewAPI group。
5. 若未来需要团队、组织、项目维度账本,应作为独立产品决策重新设计,而不是在当前 Manager 里暗自添加。
6. 子 Agnet 的运行模型是 Agnet 平台的部署配置,独立于 NewAPI 的用户、Token、Group 扣费映射;NewAPI 不负责决定子 Agnet 使用哪个模型。
6. 子 Agent 的运行模型是 Agent 平台的部署配置,独立于 NewAPI 的用户、Token、Group 扣费映射;NewAPI 不负责决定子 Agent 使用哪个模型。
推荐映射:
| Heicode/Agnet 字段 | NewAPI 映射 | 用途 |
| Heicode/Agent 字段 | NewAPI 映射 | 用途 |
|------|------|------|
| `user.id` / JWT `sub` | NewAPI user ref | 标识调用归属用户 |
| `channelId` | NewAPI channel/user/group 绑定 | 关联模型渠道、额度或扣费策略 |
| 绑定 Git 仓库 | request metadata | 审计某次开发任务来源 |
| 预算或用量限制 | token quota 或 group policy | 限制本次任务可消耗额度 |
子 Agnet 角色、运行模型和实例数量应放在 Agnet 平台 deployment/runtime 配置里,不放进 NewAPI 扣费对象里。
子 Agent 角色、运行模型和实例数量应放在 Agent 平台 deployment/runtime 配置里,不放进 NewAPI 扣费对象里。
## 五、传给 Agnet 平台的用户与扣费上下文
## 五、传给 Agent 平台的用户与扣费上下文
Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上下文,但不携带真实密钥。
Manager 请求 Agent 平台部署或执行任务时,应携带登录用户上下文,但不携带真实密钥。
建议传递结构:
@@ -161,11 +161,11 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上
"quota_ref": "newapi_token_or_group_quota_ref"
},
"agent_runtime": {
"platform": "agnet",
"platform": "agent",
"agents": [
{
"role": "backend",
"model_ref": "agnet_model_profile_backend",
"model_ref": "agent_model_profile_backend",
"instance_count": 1
}
]
@@ -190,17 +190,17 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上
}
```
这里的 `user_context` 用于确认身份,`billing_context` 用于 NewAPI 余额、用量和扣费映射,`agent_runtime` 用于 Agnet 平台独立选择子 Agnet 模型和实例数量,`work_context` 用于团队开发控制,`secret_context` 只传 `secret_ref` 和审批结果。
这里的 `user_context` 用于确认身份,`billing_context` 用于 NewAPI 余额、用量和扣费映射,`agent_runtime` 用于 Agent 平台独立选择子 Agent 模型和实例数量,`work_context` 用于团队开发控制,`secret_context` 只传 `secret_ref` 和审批结果。
Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 Azure Key Vault 凭证由平台安全通道读取,不进入 Markdown。
Agent 平台执行时,应把用户、角色、Git 绑定、deployment、Agent model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 Azure Key Vault 凭证由平台安全通道读取,不进入 Markdown。
## 六、当前主线结论
- 用户输入在 Manager/客户端,不在 NewAPI 后台。
- Manager User 复用 Heicode/Agnet 登录体系。
- Manager User 复用 Heicode/Agent 登录体系。
- Manager 不自行发明 tenant/project 作为当前团队或扣费边界。
- 团队开发控制优先由绑定 Git、允许路径、Agnet 角色、资源授权表达。
- 子 Agnet 的运行模型由 Agnet 平台独立配置,不和 NewAPI 扣费对象混在一起。
- 团队开发控制优先由绑定 Git、允许路径、Agent 角色、资源授权表达。
- 子 Agent 的运行模型由 Agent 平台独立配置,不和 NewAPI 扣费对象混在一起。
- NewAPI 负责模型网关、用户/Token/Group 额度、余额、日志和扣费。
- Azure Key Vault 负责长期密钥托管,子 Agnet 只拿短期、最小权限、可审计凭证。
- 高危操作审批发生在客户端,审批结果随任务上下文传给 Manager/Agnet 平台。
- Azure Key Vault 负责长期密钥托管,子 Agent 只拿短期、最小权限、可审计凭证。
- 高危操作审批发生在客户端,审批结果随任务上下文传给 Manager/Agent 平台。
+32 -32
View File
@@ -21,54 +21,54 @@ Heicode 的最终形态是一款覆盖软件生命周期的智能开发 Code 工
| 系统 | 定位 | 负责内容 |
|------|------|----------|
| Heicode Manager | SaaS 用户控制台与编排中枢 | 用户输入、资源绑定、权限分配、Agnet 部署、审计、模型与余额展示 |
| Agnet 平台 | 执行与状态平台 | 在 AKS 上部署子 Agnet、运行任务、维护状态、事件、日志和执行元数据 |
| Heicode Manager | SaaS 用户控制台与编排中枢 | 用户输入、资源绑定、权限分配、Agent 部署、审计、模型与余额展示 |
| Agent 平台 | 执行与状态平台 | 在 AKS 上部署子 Agent、运行任务、维护状态、事件、日志和执行元数据 |
| NewAPI | 内部模型网关与计费服务 | 模型渠道、模型调用、额度、余额、调用日志;后台不对普通用户开放 |
| Secret Store | 凭证保管库 | 保存 Git token、云密钥、SSH key、数据库密码等真实凭证 |
Manager 是用户操作入口;Agnet 是运行时执行层;NewAPI 是模型能力服务;Secret Store 是安全凭证底座。
Manager 是用户操作入口;Agent 是运行时执行层;NewAPI 是模型能力服务;Secret Store 是安全凭证底座。
## 三、不可破坏的原则
1. 在需求和边界没有想清楚前,不改代码。
2. NewAPI 保持独立服务,不继续改造成 Manager 的内嵌后台。
3. NewAPI 后台不开放给普通 SaaS 用户,模型管理由内部人员完成。
4. Manager 只补 NewAPI 没有的后端能力:用户输入编排、资源绑定、权限、Agnet 部署、审计和生命周期管理。
5. 用户绑定的是 Agnet 可用资源,不只是 Git 来源。
4. Manager 只补 NewAPI 没有的后端能力:用户输入编排、资源绑定、权限、Agent 部署、审计和生命周期管理。
5. 用户绑定的是 Agent 可用资源,不只是 Git 来源。
6. 密钥不能进入 Git、Markdown、前端、部署摘要或日志。
7. Manager 负责资源绑定、权限分配和凭证托管能力;真实密钥放入 Secret Store。
8. 子 Agnet 不保存长期密钥,只接收角色、资源元数据、AGENT.md 和受控访问方式。
9. Agnet 平台在 AKS 上负责运行时身份、隔离、状态、事件和审计回传。
10. 高危操作审批只在客户端完成;审批通过后可以把密钥保管器派生的短期、最小权限凭证注入子 Agnet,但不能注入长期密钥。
11. 子 Agnet 的运行模型是 Agnet 平台的部署配置,独立于 NewAPI 的用户、Token、Group 扣费映射。
8. 子 Agent 不保存长期密钥,只接收角色、资源元数据、AGENT.md 和受控访问方式。
9. Agent 平台在 AKS 上负责运行时身份、隔离、状态、事件和审计回传。
10. 高危操作审批只在客户端完成;审批通过后可以把密钥保管器派生的短期、最小权限凭证注入子 Agent,但不能注入长期密钥。
11. 子 Agent 的运行模型是 Agent 平台的部署配置,独立于 NewAPI 的用户、Token、Group 扣费映射。
## 四、Manager 的核心功能
1. 绑定 GitHub、GitLab、Gitea、Gitee、自建 Git 等代码来源。
2. 绑定云资源,例如 AWS、Azure、GCP、虚拟机、数据库、对象存储、Kubernetes 集群。
3. 确认项目仓库、SK 仓库、项目文档仓库或二合一仓库。
4. 按敏捷或瀑布方法分配子 Agnet 角色。
5. 为每个子 Agnet 配置 AGENT.md、可用工具、Git 范围、云资源范围、模型和预算。
6. 部署子 Agnet,设置数量、模型和运行环境。
7. 观察子 Agnet 活动状态、失败原因、事件和运行日志。
4. 按敏捷或瀑布方法分配子 Agent 角色。
5. 为每个子 Agent 配置 AGENT.md、可用工具、Git 范围、云资源范围、模型和预算。
6. 部署子 Agent,设置数量、模型和运行环境。
7. 观察子 Agent 活动状态、失败原因、事件和运行日志。
8. 查看审计日志和模型调用日志。
9. 查看可用模型、余额、额度和使用情况。
10. 展示 NewAPI 对普通用户有意义的能力,隐藏渠道、价格、模型后台管理等管理员能力。
## 五、资源绑定与密钥托管
绑定不是保存一串密钥,而是创建面向登录用户、绑定 Git/SK/云资源和子 Agnet 角色的 Resource Grant。
绑定不是保存一串密钥,而是创建面向登录用户、绑定 Git/SK/云资源和子 Agent 角色的 Resource Grant。
```text
用户授权 Heicode 使用外部资源
-> Manager 记录资源元数据
-> Manager 的 Secret Broker 把真实凭证写入 Secret Store
-> Manager 生成可审计、可撤销、可分配给子 Agnet 的资源授权
-> Manager 生成可审计、可撤销、可分配给子 Agent 的资源授权
```
资源类型:
| 类型 | 示例 | 子 Agnet 可见内容 |
| 类型 | 示例 | 子 Agent 可见内容 |
|------|------|------------------|
| Git 资源 | GitHub repo、自建 Git、SK repo | repo URL、ref、允许路径、读写范围 |
| 云账号 | Azure subscription、AWS account、GCP project | account/project/subscription 元数据、允许动作 |
@@ -87,11 +87,11 @@ Resource Binding 建议字段:
| 字段 | 含义 | 约束 |
|------|------|------|
| `id` | 资源绑定 ID | Manager 内部生成 |
| `user_id` | 登录用户 ID | 来自 Heicode/Agnet 登录体系的 `user.id` 或 JWT `sub` |
| `user_id` | 登录用户 ID | 来自 Heicode/Agent 登录体系的 `user.id` 或 JWT `sub` |
| `type` | 资源类型 | `git`、`sk`、`project_doc`、`cloud_account`、`cloud_resource` |
| `name` | 用户可见名称 | 不包含密钥 |
| `external_ref` | 外部资源定位 | repo URL、subscription ID、resource ID、文档引用等非密钥标识 |
| `metadata` | 子 Agnet 可见元数据 | 只包含 ref、允许路径、环境、网络边界等非密钥信息 |
| `metadata` | 子 Agent 可见元数据 | 只包含 ref、允许路径、环境、网络边界等非密钥信息 |
| `permission_scope` | 可授权动作范围 | 例如 `read`、`write`、`deploy`、`approve_required` |
| `constraints` | 使用限制 | 路径、分支、环境、网络、审批要求、TTL 等 |
| `secret_ref` | Secret Store 引用 | 可为空;有凭证时只保存引用,不保存原文 |
@@ -105,8 +105,8 @@ Resource Grant 建议字段:
| `id` | 授权 ID | Manager 内部生成 |
| `user_id` / `binding_scope` | 授权归属 | 来自登录用户、绑定 Git/SK/云资源和角色范围 |
| `resource_id` | 被授权资源 | 指向 Resource Binding |
| `role` | 子 Agnet 角色 | 例如 product、frontend、backend、reviewer、ops |
| `agent_id` | 子 Agnet 标识 | 可为空;为空表示授予该项目角色下的下一次部署 |
| `role` | 子 Agent 角色 | 例如 product、frontend、backend、reviewer、ops |
| `agent_id` | 子 Agent 标识 | 可为空;为空表示授予该项目角色下的下一次部署 |
| `allowed_actions` | 本次授权动作 | 必须是 `permission_scope` 的子集 |
| `constraints` | 本次授权限制 | 不得放宽 Resource Binding 的限制 |
| `status` | 授权状态 | `active`、`suspended`、`revoked`、`expired` |
@@ -146,7 +146,7 @@ SaaS 场景下,凭证不能转嫁给用户手工管理。用户负责授权,
| 方案 | 判断 |
|------|------|
| HashiCorp Vault | 优先选择。Kubernetes Auth、Policy、TTL、动态密钥、审计能力成熟,适合 AKS 中的子 Agnet 运行时授权 |
| HashiCorp Vault | 优先选择。Kubernetes Auth、Policy、TTL、动态密钥、审计能力成熟,适合 AKS 中的子 Agent 运行时授权 |
| Infisical | 可选方案。产品体验较好,但需要验证 SaaS 多用户隔离策略和运行时授权能力 |
| Azure Key Vault | 适合 Azure 优先部署,也可以作为 Secret Provider 的一种实现 |
@@ -160,9 +160,9 @@ Secret Broker 负责:
- 轮换、撤销、禁用凭证。
- 避免密钥进入日志、前端响应、Markdown 和 Git。
## 七、AKS 上的 Agnet 凭证访问
## 七、AKS 上的 Agent 凭证访问
Agnet 平台底层是 AKS,因此运行时权限应和 Kubernetes 身份绑定。
Agent 平台底层是 AKS,因此运行时权限应和 Kubernetes 身份绑定。
推荐流程:
@@ -170,13 +170,13 @@ Agnet 平台底层是 AKS,因此运行时权限应和 Kubernetes 身份绑定
用户在 Manager 授权资源
-> Manager Secret Broker 写入 Secret Store
-> Manager 记录 Resource Grant
-> Manager 请求 Agnet 平台部署
-> Agnet 平台为 deployment / role 创建 K8s ServiceAccount
-> Agnet 平台绑定 Vault policy 或 Workload Identity
-> 子 Agnet Pod 运行时只能访问被授权的 secret
-> Manager 请求 Agent 平台部署
-> Agent 平台为 deployment / role 创建 K8s ServiceAccount
-> Agent 平台绑定 Vault policy 或 Workload Identity
-> 子 Agent Pod 运行时只能访问被授权的 secret
```
子 Agnet 拿到的是角色、目标、AGENT.md、resource context 和 permission manifest,不拿长期密钥。
子 Agent 拿到的是角色、目标、AGENT.md、resource context 和 permission manifest,不拿长期密钥。
运行时访问分两类:
@@ -209,7 +209,7 @@ Manager 不展示:
- 系统管理员用户管理。
- NewAPI 原生管理后台入口。
用户登录 Manager,不直接登录 NewAPI。Manager User 复用 Heicode/Agnet 登录体系,并维护 `user.id`、`channelId` 到 NewAPI 用户、Token、Group、quota 或 usage 的映射。当前不要把 Manager tenant/project 作为扣费和团队开发控制主轴;团队开发控制优先由绑定 Git、路径范围、资源授权和 Agnet 角色表达。子 Agnet 的模型选择、模型 profile 和实例数属于 Agnet 平台部署配置,不放进 NewAPI 扣费对象里。
用户登录 Manager,不直接登录 NewAPI。Manager User 复用 Heicode/Agent 登录体系,并维护 `user.id`、`channelId` 到 NewAPI 用户、Token、Group、quota 或 usage 的映射。当前不要把 Manager tenant/project 作为扣费和团队开发控制主轴;团队开发控制优先由绑定 Git、路径范围、资源授权和 Agent 角色表达。子 Agent 的模型选择、模型 profile 和实例数属于 Agent 平台部署配置,不放进 NewAPI 扣费对象里。
## 九、Markdown 与权限清单
@@ -217,7 +217,7 @@ Manager 不展示:
Markdown 可包含:
- 子 Agnet 角色。
- 子 Agent 角色。
- 目标任务。
- 项目背景。
- AGENT.md 来源。
@@ -240,7 +240,7 @@ Manager 应生成两类产物:
| 产物 | 用途 |
|------|------|
| AGENT.md / resource context | 给子 Agnet 的启动上下文,说明角色和可用资源 |
| permission manifest | 给 Agnet 平台和审计系统的结构化权限清单 |
| AGENT.md / resource context | 给子 Agent 的启动上下文,说明角色和可用资源 |
| permission manifest | 给 Agent 平台和审计系统的结构化权限清单 |
Markdown 面向模型理解,manifest 面向系统强制执行。
@@ -1,7 +1,7 @@
# Agent Manager 普通 sub 产物回调缺失问题
更新时间:2026-05-29
发给:Agent Manager / Agnet Runtime 负责人
发给:Agent Manager / Agent Runtime 负责人
范围:普通 sub 敏捷模式,不包含 HeiCode-Swarm 独立蜂群 Runtime。
## 1. 问题现象
@@ -36,8 +36,8 @@ SK SNAPSHOTS = 0
Heicode Manager 生产配置确认当前普通 sub 走:
```text
AGNET_RUNTIME_BASE_URL=http://20.212.121.126
AGNET_RUNTIME_CREATE_PATH=/api/swarms
AGENT_RUNTIME_BASE_URL=http://20.212.121.126
AGENT_RUNTIME_CREATE_PATH=/api/swarms
```
`SWARM_RUNTIME_BASE_URL` 当前为空。
@@ -47,7 +47,7 @@ AGNET_RUNTIME_CREATE_PATH=/api/swarms
```text
Heicode 桌面客户端
-> Heicode Manager 生产
-> Agent Manager / Agnet Runtime 普通 sub 入口
-> Agent Manager / Agent Runtime 普通 sub 入口
-> Heicode Manager callback
-> 桌面客户端查询 artifacts/timeline
```
@@ -102,7 +102,7 @@ sk_tool.completed = 0
Heicode Manager artifact 表查询结果:
```text
agnet_artifacts where deployment_id = 'dep_fa4f43da9e0a'
agent_artifacts where deployment_id = 'dep_fa4f43da9e0a'
=> 0 rows
```
@@ -113,7 +113,7 @@ agnet_artifacts where deployment_id = 'dep_fa4f43da9e0a'
生产日志中,Agent Manager 在任务完成时间段连续请求:
```text
POST /api/agnet/callbacks/swarm-events
POST /api/agent/callbacks/swarm-events
```
HTTP 状态均为:
@@ -188,7 +188,7 @@ Logs will be fetched from K8s in Phase 2
本问题根因不在 Heicode Manager,也不在桌面客户端。
当前证据指向 Agent Manager / Agnet Runtime:
当前证据指向 Agent Manager / Agent Runtime:
1. Runtime 将 deployment 标记为 `completed`。
2. Runtime 自身返回 `artifacts: []`。
@@ -206,7 +206,7 @@ Logs will be fetched from K8s in Phase 2
普通 sub 任务完成时,Agent Manager 必须向 Heicode Manager callback:
```text
POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
POST https://code.xinghanlab.com/api/agent/callbacks/swarm-events
event_type = artifact.created
```
@@ -370,7 +370,7 @@ Logs will be fetched from K8s in Phase 2
|---|---|
| Runtime 状态 | deployment 和 agents 状态一致 |
| callback | Heicode Manager 收到 `artifact.created` |
| Manager artifacts | `/api/agnet/user/deployments/{deployment_id}/artifacts` 返回 total > 0 |
| Manager artifacts | `/api/agent/user/deployments/{deployment_id}/artifacts` 返回 total > 0 |
| 客户端展示 | 右侧 `ARTIFACTS` 不再是 0 |
| 交付物 | 能看到 Git 分支、提交、预览地址、部署清单或最终结果文档 |
| usage | 如果真实调用模型,tokens/cost 不应一直为 0 |
@@ -380,7 +380,7 @@ Logs will be fetched from K8s in Phase 2
Heicode Manager 和桌面客户端当前表现是正确反映 Runtime 数据。
真正缺口在 Agent Manager / Agnet Runtime:
真正缺口在 Agent Manager / Agent Runtime:
```text
任务被标记 completed,但 Runtime 没有生成或回传 artifact.created。
@@ -1,7 +1,7 @@
# Agent Manager 普通 sub 剩余补充要求
更新时间:2026-05-28
发给:Agent Manager / Agnet Runtime 负责人
发给:Agent Manager / Agent Runtime 负责人
范围:普通 sub 敏捷开发模式,不包含蜂群模式完整 task graph。
## 1. 当前已验证事实
@@ -59,7 +59,7 @@ Heicode Manager 生产版本 `1.4.19` 已完成并验证以下链路:
Manager callback 地址:
```text
https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
https://code.xinghanlab.com/api/agent/callbacks/swarm-events
```
创建 run 后,Agent Manager 至少需要主动回调这些事件:
@@ -224,7 +224,7 @@ Agent Manager 补完后,按以下步骤验收:
- `artifact.created`
- `budget.alert`
5. Manager 查询:
- `/api/agnet/user/deployments/{deployment_id}`
- `/api/agent/user/deployments/{deployment_id}`
- `/events`
- `/timeline`
- `/artifacts`
@@ -1,7 +1,7 @@
# Agent Manager 普通 sub 联调整改要求
更新时间:2026-05-28
发给:Agent Manager / Agnet Runtime 负责人
发给:Agent Manager / Agent Runtime 负责人
范围:Heicode Manager 普通 sub 敏捷模式联调,不包含蜂群模式完整 task graph 的额外能力。
## 1. 当前结论
@@ -10,7 +10,7 @@ Agent Manager 新版文档 `HEICODE_API_INTEGRATION(5).md` 已经补充了 Runti
按 2026-05-28 生产实测,Manager 与 Agent Manager 的普通 sub 核心通讯链路已经跑通,但还不能认为“真实开发执行结果”完整闭环。当前结论是:
1. Agent Manager 当前联调统一使用 IP `http://20.212.121.126`;域名 `https://agent-manager.taijiagnet.com` 后续解析和证书就绪后再切换,不作为当前联调阻塞项。
1. Agent Manager 当前联调统一使用 IP `http://20.212.121.126`;域名 `https://agent-manager.taijiagent.com` 后续解析和证书就绪后再切换,不作为当前联调阻塞项。
2. Manager 生产已切到 `/api/swarms` 创建入口,并验证能拿到 `runtime_swarm_id=swm_*`。
3. Agent Manager 自动 callback 已能写入 Manager `events/timeline`,Manager `1.4.19` 开始会把 callback 反写到 deployment 快照。
4. 当前仍缺真实产物和真实用量:Agent Manager status 接口返回 `tokens_used=0`、`artifacts=[]`,并出现整体 `completed` 但 agent 仍 `running` 的状态不一致。
@@ -18,9 +18,9 @@ Agent Manager 新版文档 `HEICODE_API_INTEGRATION(5).md` 已经补充了 Runti
Heicode Manager 侧已确认:
- `POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events` 生产路由在线。
- `POST https://code.xinghanlab.com/api/agent/callbacks/swarm-events` 生产路由在线。
- callback 支持 HMAC 和旧 token 兼容;当前生产为了先跑通自动 callback,已配置 HMAC fallback。
- `GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema` 已由 Manager 提供,用于联调前核对事件类型和必填字段。
- `GET https://code.xinghanlab.com/api/agent/callbacks/swarm-events/schema` 已由 Manager 提供,用于联调前核对事件类型和必填字段。
- Manager 可接收 `deployment.status_changed`、`phase.changed`、`timeline.updated`、`agent.started/completed/crashed`,并在 `1.4.19` 反写 deployment 详情状态。
## 2. Agent Manager 必须整改的 P0
@@ -30,7 +30,7 @@ Heicode Manager 侧已确认:
| P0 | 固定联调地址 | 当前阶段约定先走 IP,后续再切域名 | 文档和配置先统一使用 `http://20.212.121.126`;域名切换另行确认 |
| P0 | 创建普通 sub deployment | `/api/swarms` 已能创建并返回 `swm_*` | 保持幂等和字段稳定,后续域名切换不能破坏 |
| P0 | deployment detail | Manager 已能记录 callback;Agent Manager status 仍有 `completed` 与 agent `running` 不一致 | 返回真实 phase、agent 状态、失败原因和更新时间 |
| P0 | deployment stop | `/api/agnet/deployments/{deployment_id}/stop` 已可用 | 停止后 Runtime 真实停止,并回调 stopped 事件 |
| P0 | deployment stop | `/api/agent/deployments/{deployment_id}/stop` 已可用 | 停止后 Runtime 真实停止,并回调 stopped 事件 |
| P0 | callback 主动推送 | 已验证 status/phase/timeline/agent/budget callback 能进入 Manager | 继续补 artifact、SK、approval 的真实运行数据 |
| P0 | usage 回传 | 当前 budget callback 中 token/cost/runtime 多为 0 | 通过 callback 或事件接口回传真实 token、成本、运行时长、资源使用 |
@@ -39,7 +39,7 @@ Heicode Manager 侧已确认:
### 3.1 健康检查
```http
GET /api/agnet/health
GET /api/agent/health
```
要求:
@@ -50,10 +50,10 @@ GET /api/agnet/health
### 3.2 创建普通 sub 运行
普通 sub 当前生产联调使用 Agent Manager `/api/agnet/deployments`,蜂群 `/api/swarms` 另见 `AgentManager蜂群Runtime接口实现要求.md`。
普通 sub 当前生产联调使用 Agent Manager `/api/agent/deployments`,蜂群 `/api/swarms` 另见 `AgentManager蜂群Runtime接口实现要求.md`。
```http
POST /api/agnet/deployments
POST /api/agent/deployments
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
Content-Type: application/json
X-User-ID: <user_id>
@@ -115,8 +115,8 @@ X-Idempotency-Key: <idempotency_key>
"resource_grants": []
},
"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",
"url": "https://code.xinghanlab.com/api/agent/callbacks/swarm-events",
"signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agent-callback-signing-key",
"subscribed_events": [
"deployment.status_changed",
"phase.changed",
@@ -154,14 +154,14 @@ X-Idempotency-Key: <idempotency_key>
要求:
1. `deployment_id` 必须可用于 `/api/agnet/deployments/{deployment_id}`、`/api/agnet/deployments/{deployment_id}/stop`。
1. `deployment_id` 必须可用于 `/api/agent/deployments/{deployment_id}`、`/api/agent/deployments/{deployment_id}/stop`。
2. 同一个 `X-Idempotency-Key` 重复请求必须返回同一个运行,不应重复创建。
3. 请求缺少 `orchestration_plan`、`callback.url`、`sub_mode`、`user_context` 时必须返回 4xx。
### 3.3 查询普通 sub deployment
```http
GET /api/agnet/deployments/{deployment_id}
GET /api/agent/deployments/{deployment_id}
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
```
@@ -188,7 +188,7 @@ Authorization: Bearer <HEICODE_SERVICE_TOKEN>
### 3.4 停止普通 sub deployment
```http
POST /api/agnet/deployments/{deployment_id}/stop
POST /api/agent/deployments/{deployment_id}/stop
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
Content-Type: application/json
```
@@ -223,7 +223,7 @@ Content-Type: application/json
Manager 生产 callback 地址:
```text
https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
https://code.xinghanlab.com/api/agent/callbacks/swarm-events
```
### 4.1 认证方式
@@ -231,9 +231,9 @@ https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
优先使用 HMAC:
```http
X-Agnet-Event-Id: evt_xxx
X-Agnet-Timestamp: <unix_ms>
X-Agnet-Signature: sha256=<hex>
X-Agent-Event-Id: evt_xxx
X-Agent-Timestamp: <unix_ms>
X-Agent-Signature: sha256=<hex>
X-Correlation-ID: <correlation_id>
Content-Type: application/json
```
@@ -248,7 +248,7 @@ signature = HMAC_SHA256(callback_signing_secret, signature_payload)
过渡期可使用旧 token:
```http
X-Agnet-Service-Token: <HEICODE_SERVICE_TOKEN>
X-Agent-Service-Token: <HEICODE_SERVICE_TOKEN>
```
不要把真实 token 写入文档、日志或 artifact。
@@ -397,7 +397,7 @@ Manager 端已有 approval、credential lease、approve/reject 和 Runtime decis
推荐接口:
```http
POST /api/agnet/deployments/{deployment_id}/approvals/{approval_id}
POST /api/agent/deployments/{deployment_id}/approvals/{approval_id}
Authorization: Bearer <HEICODE_SERVICE_TOKEN>
Content-Type: application/json
```
@@ -415,7 +415,7 @@ Content-Type: application/json
"resource_id": "repo_main",
"risk_level": "high",
"requires_credential": true,
"credential_ref": "lease://agnet/cred_123",
"credential_ref": "lease://agent/cred_123",
"lease_id": "lease_123",
"lease_expires_at": 1770000000000,
"decided_by": "user_123",
@@ -454,10 +454,10 @@ Agent Manager 改完后,按下面步骤验收。
| 步骤 | 操作 | 通过标准 |
|---:|---|---|
| 1 | `GET /api/agnet/health` | 返回 healthy |
| 2 | `POST /api/agnet/deployments` 创建普通 sub run | 返回 `deployment_id`、`status` |
| 1 | `GET /api/agent/health` | 返回 healthy |
| 2 | `POST /api/agent/deployments` 创建普通 sub run | 返回 `deployment_id`、`status` |
| 3 | 重复同一个 `X-Idempotency-Key` 创建 | 不重复创建,返回同一个 ID |
| 4 | `GET /api/agnet/deployments/{deployment_id}` | 返回真实状态和 agents |
| 4 | `GET /api/agent/deployments/{deployment_id}` | 返回真实状态和 agents |
| 5 | Runtime 主动 callback `phase.changed` | Manager callback 返回 success |
| 6 | Runtime 主动 callback `timeline.updated` | Manager timeline 可查到 |
| 7 | Runtime 主动 callback `artifact.created` | Manager artifacts 可查到 |
@@ -465,7 +465,7 @@ Agent Manager 改完后,按下面步骤验收。
| 9 | Runtime 主动 callback usage | Manager 能看到 usage 摘要或 callback event |
| 10 | Runtime 主动 callback `approval.requested` | Manager pending approval 生成 |
| 11 | Manager approve/reject | Agent Manager 收到 decision |
| 12 | `POST /api/agnet/deployments/{deployment_id}/stop` | Runtime 停止,状态变为 stopped |
| 12 | `POST /api/agent/deployments/{deployment_id}/stop` | Runtime 停止,状态变为 stopped |
| 13 | 检查日志和 artifact metadata | 不包含明文密钥 |
## 9. 当前实测记录
@@ -474,12 +474,12 @@ Agent Manager 改完后,按下面步骤验收。
| 接口 | 结果 | 说明 |
|---|---|---|
| `GET http://20.212.121.126/api/agnet/health` | 200 | Agent Manager IP 健康检查正常 |
| `GET https://agent-manager.taijiagnet.com/api/agnet/health` | 暂不作为当前验收项 | 当前约定先走 IP,域名后续再切换 |
| `POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events` 无认证 | 业务返回 `CALLBACK_UNAUTHORIZED` | Manager callback 路由在线 |
| `POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events` 带旧 token 但空 body | 业务返回 `CALLBACK_INVALID` | 认证通过,进入事件校验 |
| `GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema` | 404 | Manager schema GET 生产未通,Manager 侧需复核 |
| `POST http://20.212.121.126/api/agnet/deployments` | 200 | 普通 sub Runtime deployment 创建可用 |
| `GET http://20.212.121.126/api/agent/health` | 200 | Agent Manager IP 健康检查正常 |
| `GET https://agent-manager.taijiagent.com/api/agent/health` | 暂不作为当前验收项 | 当前约定先走 IP,域名后续再切换 |
| `POST https://code.xinghanlab.com/api/agent/callbacks/swarm-events` 无认证 | 业务返回 `CALLBACK_UNAUTHORIZED` | Manager callback 路由在线 |
| `POST https://code.xinghanlab.com/api/agent/callbacks/swarm-events` 带旧 token 但空 body | 业务返回 `CALLBACK_INVALID` | 认证通过,进入事件校验 |
| `GET https://code.xinghanlab.com/api/agent/callbacks/swarm-events/schema` | 404 | Manager schema GET 生产未通,Manager 侧需复核 |
| `POST http://20.212.121.126/api/agent/deployments` | 200 | 普通 sub Runtime deployment 创建可用 |
### 9.1 2026-05-28 追加联调记录
@@ -491,17 +491,17 @@ Agent Manager 改完后,按下面步骤验收。
- 因此本次没有宣称“桌面客户端登录后走 Manager 用户态创建 deployment”完整通过。
- 本次完成的是服务到服务联调:Manager/客户端等价 payload -> Agent Manager IP,以及 Agent Manager callback 协议 -> Heicode Manager callback 接收端。
Agent Manager `/api/agnet/deployments` 服务到服务链路:
Agent Manager `/api/agent/deployments` 服务到服务链路:
| 步骤 | 结果 | 证据 |
|---|---|---|
| `GET /api/agnet/health` | 通过 | 返回 `success=true`、`status=healthy` |
| `POST /api/agnet/deployments` | 通过 | 返回 `deployment_id=dep_dbafd1ac37c3`,状态 `pending` |
| `GET /api/agnet/deployments/{deployment_id}` | 通过 | 能查到 `user_id`、`binding_scope`、`sub_mode=agile`、`callback_configured=true` |
| `GET /api/agnet/deployments/{deployment_id}/events` | 部分通过 | 只看到 `deployment.accepted`,未看到 `phase.changed/timeline.updated/artifact.created/sk_tool.*` |
| `GET /api/agnet/deployments/{deployment_id}/logs` | 部分通过 | 返回系统日志 `Pod ... has no logs yet` |
| `GET /api/agnet/deployments/{deployment_id}/metrics` | 通过但疑似占位 | 返回 CPU/内存/网络指标,但值看起来是固定模拟值,需要 Runtime 说明来源 |
| `POST /api/agnet/deployments/{deployment_id}/stop` | 通过 | `dep_dbafd1ac37c3` 停止成功,返回 `status=stopped` |
| `GET /api/agent/health` | 通过 | 返回 `success=true`、`status=healthy` |
| `POST /api/agent/deployments` | 通过 | 返回 `deployment_id=dep_dbafd1ac37c3`,状态 `pending` |
| `GET /api/agent/deployments/{deployment_id}` | 通过 | 能查到 `user_id`、`binding_scope`、`sub_mode=agile`、`callback_configured=true` |
| `GET /api/agent/deployments/{deployment_id}/events` | 部分通过 | 只看到 `deployment.accepted`,未看到 `phase.changed/timeline.updated/artifact.created/sk_tool.*` |
| `GET /api/agent/deployments/{deployment_id}/logs` | 部分通过 | 返回系统日志 `Pod ... has no logs yet` |
| `GET /api/agent/deployments/{deployment_id}/metrics` | 通过但疑似占位 | 返回 CPU/内存/网络指标,但值看起来是固定模拟值,需要 Runtime 说明来源 |
| `POST /api/agent/deployments/{deployment_id}/stop` | 通过 | `dep_dbafd1ac37c3` 停止成功,返回 `status=stopped` |
长等待测试:
@@ -535,7 +535,7 @@ source=agent-manager-ip-smoke
结论:
1. Heicode Manager callback 接收端可用,旧 token 认证和幂等可用。
2. Agent Manager IP 的 `/api/agnet/deployments` 创建、查询、停止可用。
2. Agent Manager IP 的 `/api/agent/deployments` 创建、查询、停止可用。
3. Agent Manager 当前没有在实测中产生普通 sub 所需的 phase/timeline/artifact/SK 主动回调。
4. 因没有可用生产 Manager 用户 session,本次没有完成“客户端登录态 -> Manager 用户态 deployment -> Runtime”的完整端到端测试。
@@ -558,15 +558,15 @@ group: default
|---|---|---|
| 登录生产 Manager | 通过 | `/api/user/login` 返回 `success=true`,用户 `id=22` |
| 查询用户信息 | 通过 | `/api/user/self` 返回用户 `chenchen`、`group=default` |
| 查询 Runtime 健康 | 通过 | `/api/agnet/runtime/health` 返回 `enabled=true`、`configured=true`、`status=healthy`、`create_path=/api/agnet/deployments`、远端为 Agent Manager IP |
| 客户端模拟创建 deployment | 通过 | `POST /api/agnet/user/deployments` 返回 Manager deployment `dep_1ba14ccfb558` |
| 查询 Runtime 健康 | 通过 | `/api/agent/runtime/health` 返回 `enabled=true`、`configured=true`、`status=healthy`、`create_path=/api/agent/deployments`、远端为 Agent Manager IP |
| 客户端模拟创建 deployment | 通过 | `POST /api/agent/user/deployments` 返回 Manager deployment `dep_1ba14ccfb558` |
| Manager -> Agent Manager create | 通过 | Manager detail 写回 `runtime_deployment_id=dep_3335e54e9bdf`,`runtime_state=pending` |
| Manager 用户态 events | 通过 | 出现 `deployment.accepted`、`runtime.sync.started`、`runtime.sync.accepted` |
| Manager 用户态 logs | 通过 | 出现 control-plane 日志和 runtime sync 日志,均为 redacted |
| Manager 用户态 metrics | 部分通过 | 返回 `platform_estimated=true`,说明是 Manager 估算/占位,不是 Runtime 真实资源指标 |
| Manager 用户态 artifacts | 未产出 | 返回空列表 |
| Manager 用户态 timeline | 部分通过 | 能返回 deployment/events,但 callbacks/artifacts 为空 |
| Manager stop | 通过 | `POST /api/agnet/user/deployments/dep_1ba14ccfb558/stop` 返回 `status=stopped` |
| Manager stop | 通过 | `POST /api/agent/user/deployments/dep_1ba14ccfb558/stop` 返回 `status=stopped` |
| Runtime stop 结果 | 通过 | 直查 Agent Manager `dep_3335e54e9bdf`,状态为 `stopped` |
本次真实 ID:
@@ -582,10 +582,10 @@ Agent Manager 侧直查 `dep_3335e54e9bdf`:
| 接口 | 结果 |
|---|---|
| `GET /api/agnet/deployments/dep_3335e54e9bdf` | 200,状态 `stopped`,`user_id=22` |
| `GET /api/agnet/deployments/dep_3335e54e9bdf/events` | 200,仅有 `deployment.accepted`、`deployment.stopped` |
| `GET /api/agnet/deployments/dep_3335e54e9bdf/logs` | 200,仅有 `Pod ... has no logs yet` |
| `GET /api/agnet/deployments/dep_3335e54e9bdf/metrics` | 200,返回 CPU/内存/网络固定值 |
| `GET /api/agent/deployments/dep_3335e54e9bdf` | 200,状态 `stopped`,`user_id=22` |
| `GET /api/agent/deployments/dep_3335e54e9bdf/events` | 200,仅有 `deployment.accepted`、`deployment.stopped` |
| `GET /api/agent/deployments/dep_3335e54e9bdf/logs` | 200,仅有 `Pod ... has no logs yet` |
| `GET /api/agent/deployments/dep_3335e54e9bdf/metrics` | 200,返回 CPU/内存/网络固定值 |
本次客户端模拟链路结论:
@@ -31,18 +31,18 @@ Agent Manager / Swarm Runtime 需要负责执行层:
当前生产 Manager Runtime 配置实际为:
```text
AGNET_RUNTIME_BASE_URL=http://20.212.121.126
AGNET_RUNTIME_CREATE_PATH=/api/agnet/deployments
AGNET_RUNTIME_STOP_PATH=/api/agnet/deployments/{deployment_id}/stop
AGENT_RUNTIME_BASE_URL=http://20.212.121.126
AGENT_RUNTIME_CREATE_PATH=/api/agent/deployments
AGENT_RUNTIME_STOP_PATH=/api/agent/deployments/{deployment_id}/stop
```
实测结果:
| 项 | 结果 |
|---|---|
| `GET /api/agnet/runtime/health` | 通过,远端 Agent Manager IP healthy |
| `POST /api/swarms` 使用 `agent_runtime.platform=agnet-swarm` | 失败,Manager 返回 `agent_runtime.platform must be agnet when runtime context is present` |
| `POST /api/swarms` 使用 `agent_runtime.platform=agnet` | 通过 |
| `GET /api/agent/runtime/health` | 通过,远端 Agent Manager IP healthy |
| `POST /api/swarms` 使用 `agent_runtime.platform=agent-swarm` | 失败,Manager 返回 `agent_runtime.platform must be agent when runtime context is present` |
| `POST /api/swarms` 使用 `agent_runtime.platform=agent` | 通过 |
| Manager deployment | `dep_72cdc138b9c8` |
| Manager 返回 swarm_id | `dep_72cdc138b9c8`,当前是 Manager 本地 deployment id |
| Runtime deployment | `dep_77370f27186f` |
@@ -58,7 +58,7 @@ AGNET_RUNTIME_STOP_PATH=/api/agnet/deployments/{deployment_id}/stop
结论:
1. Heicode Manager 的 `/api/swarms` adapter 能接收用户态蜂群请求,并能通过当前 Runtime bridge 调 Agent Manager IP。
2. 当前生产链路实际走的是 Agent Manager `/api/agnet/deployments`,不是 Agent Manager `/api/swarms`。
2. 当前生产链路实际走的是 Agent Manager `/api/agent/deployments`,不是 Agent Manager `/api/swarms`。
3. Manager stop 可以传递到 Agent Manager deployment stop。
4. 当前还没有真实蜂群 Runtime callback、task graph、artifact、SK、usage。
5. Agent Manager 如果要满足正式蜂群模式,仍需实现本文后续定义的 `/api/swarms`、`/api/swarms/{swarm_id}`、`/api/swarms/{swarm_id}/stop`、callback 和 usage 约束。
@@ -68,13 +68,13 @@ AGNET_RUNTIME_STOP_PATH=/api/agnet/deployments/{deployment_id}/stop
```text
Heicode Desktop Client
-> Heicode Manager
POST /api/swarms 或 /api/agnet/user/deployments
POST /api/swarms 或 /api/agent/user/deployments
-> Agent Manager / Swarm Runtime
POST /api/swarms
<- Runtime response
runtime_deployment_id / swarm_id / status
<- Runtime callback
POST /api/agnet/callbacks/swarm-events
POST /api/agent/callbacks/swarm-events
-> Runtime approval decision
POST /api/swarms/{swarm_id}/approvals/{approval_id}
```
@@ -98,11 +98,11 @@ Content-Type: application/json
| 优先级 | 方法 | 路径 | 必须 | 用途 |
|---|---|---|---:|---|
| P0 | `GET` | `/api/agnet/health` | 是 | 健康检查 |
| P0 | `GET` | `/api/agent/health` | 是 | 健康检查 |
| P0 | `POST` | `/api/swarms` | 是 | 创建真实 Swarm Run |
| P0 | `POST` | `/api/swarms/{swarm_id}/stop` | 是 | 停止 Swarm Run |
| P0 | `POST` | `/api/swarms/{swarm_id}/approvals/{approval_id}` | 是 | 接收 Manager 审批决定 |
| P0 | callback | Manager `/api/agnet/callbacks/swarm-events` | 是 | 回写状态、task、handoff、artifact、approval |
| P0 | callback | Manager `/api/agent/callbacks/swarm-events` | 是 | 回写状态、task、handoff、artifact、approval |
| P1 | `GET` | `/api/swarms/{swarm_id}` | 建议 | 查询 Runtime 详情 |
| P1 | `GET` | `/api/swarms/{swarm_id}/tasks` | 建议 | 查询 task graph |
| P1 | `GET` | `/api/swarms/{swarm_id}/logs` | 建议 | 查询日志 |
@@ -130,7 +130,7 @@ Content-Type: application/json
3. `X-Idempotency-Key` 必须支持幂等;重复创建请求不能生成多个真实 Swarm Run。
4. Agent Manager 不得要求 Manager 传长期明文密钥。
## 5. `GET /api/agnet/health`
## 5. `GET /api/agent/health`
### 响应
@@ -227,8 +227,8 @@ Content-Type: application/json
}
],
"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",
"url": "https://code.xinghanlab.com/api/agent/callbacks/swarm-events",
"signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agent-callback-signing-key",
"subscribed_events": [
"deployment.status_changed",
"task.created",
@@ -342,9 +342,9 @@ Agent Manager 创建 Swarm Run 后,必须在 Runtime 内部生成任务图,
Manager 回调地址:
```http
POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
X-Agnet-Service-Token: <callback_token>
X-Agnet-Event-Id: <event_id>
POST https://code.xinghanlab.com/api/agent/callbacks/swarm-events
X-Agent-Service-Token: <callback_token>
X-Agent-Event-Id: <event_id>
X-Correlation-ID: <correlation_id>
Content-Type: application/json
```
@@ -352,8 +352,8 @@ Content-Type: application/json
也支持 HMAC:
```http
X-Agnet-Timestamp: <unix_ms>
X-Agnet-Signature: sha256=<hex>
X-Agent-Timestamp: <unix_ms>
X-Agent-Signature: sha256=<hex>
```
HMAC 签名内容:
@@ -365,7 +365,7 @@ timestamp + "." + event_id + "." + raw_body
联调前可拉取 Manager 当前接受的事件 schema:
```http
GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema
GET https://code.xinghanlab.com/api/agent/callbacks/swarm-events/schema
```
### 通用 callback envelope
@@ -647,7 +647,7 @@ Runtime 可通过 `budget.alert` 或 `timeline.updated` 回传用量摘要。
| 步骤 | 操作 | 通过标准 |
|---:|---|---|
| 1 | Manager 调 `GET /api/agnet/health` | 返回 healthy |
| 1 | Manager 调 `GET /api/agent/health` | 返回 healthy |
| 2 | Manager 调 `POST /api/swarms` | 返回真实 `deployment_id` 和 `swarm_id` |
| 3 | Runtime 回调 `deployment.status_changed` | Manager timeline 可见 |
| 4 | Runtime 回调 `task.created` | Manager task flow / Agent task map 可见 |
@@ -10,10 +10,10 @@
| 模式 | Runtime | 当前已知地址 | 说明 |
|---|---|---|---|
| 普通 sub 敏捷模式 | Agent Manager / Agnet Runtime | `http://20.212.121.126` | 用于普通子 Agent 敏捷开发流程 |
| 普通 sub 敏捷模式 | Agent Manager / Agent Runtime | `http://20.212.121.126` | 用于普通子 Agent 敏捷开发流程 |
| 蜂群模式 | HeiCode-Swarm Orchestrator | `http://52.139.240.116:8000` | 用于蜂群任务图、Agent 协作、handoff、task graph |
注意:两套服务可能都提供 `/api/swarms` 这类路径,但业务含义不同。Heicode Manager 后续需要按模式分别配置,不应只用一套 `AGNET_RUNTIME_BASE_URL` 混跑。
注意:两套服务可能都提供 `/api/swarms` 这类路径,但业务含义不同。Heicode Manager 后续需要按模式分别配置,不应只用一套 `AGENT_RUNTIME_BASE_URL` 混跑。
## 2. 本次读取到的蜂群 Runtime 新能力
@@ -21,9 +21,9 @@
| 能力 | 路径 | 当前判断 |
|---|---|---|
| 健康检查 | `GET /api/agnet/health` | 已提供 |
| 健康检查 | `GET /api/agent/health` | 已提供 |
| 创建蜂群 run | `POST /api/swarms` | 已提供 |
| 兼容创建入口 | `POST /api/agnet/deployments` | 已提供 |
| 兼容创建入口 | `POST /api/agent/deployments` | 已提供 |
| 查询 swarm 详情 | `GET /api/swarms/{swarm_id}` | 已提供 |
| 查询任务图 | `GET /api/swarms/{swarm_id}/tasks` | 已提供 |
| 查询日志 | `GET /api/swarms/{swarm_id}/logs` | 已提供 |
@@ -48,7 +48,7 @@ runtime contract checks passed
请求:
```bash
curl http://52.139.240.116:8000/api/agnet/health
curl http://52.139.240.116:8000/api/agent/health
```
返回核心内容:
@@ -247,7 +247,7 @@ allow_handoff=false
| 项 | 需要提供 / 确认 | 原因 |
|---|---|---|
| 蜂群 Runtime 调用 token | 已提供并验证可用;后续需要通过安全渠道配置到 Manager,不写入 Git | Manager 调 `POST /api/swarms` 需要 Bearer 鉴权 |
| callback 凭据配置 | Runtime 配置 Manager 认可的 `AGNET_CALLBACK_SERVICE_TOKEN` 或 `AGNET_CALLBACK_SIGNING_SECRET` | Runtime 回调 Manager 必须通过鉴权 |
| callback 凭据配置 | Runtime 配置 Manager 认可的 `AGENT_CALLBACK_SERVICE_TOKEN` 或 `AGENT_CALLBACK_SIGNING_SECRET` | Runtime 回调 Manager 必须通过鉴权 |
| 两套部署边界 | 明确普通 sub 继续走 `20.212.121.126`,蜂群走 `52.139.240.116:8000` | 防止普通 sub 和蜂群混用 |
| 创建响应字段 | `deployment_id`、`runtime_deployment_id`、`swarm_id`、`status` 字段保持稳定 | Manager 需要保存映射 |
| callback deployment id 规则 | callback 中 `deployment_id` 应为 Manager deployment id,`runtime_deployment_id` 为 Runtime id,`swarm_id` 为 Runtime swarm id | Manager 根据这些字段落库和展示 |
@@ -255,13 +255,13 @@ allow_handoff=false
callback 地址:
```text
https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
https://code.xinghanlab.com/api/agent/callbacks/swarm-events
```
callback schema 查询:
```text
GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema
GET https://code.xinghanlab.com/api/agent/callbacks/swarm-events/schema
```
说明:具体 token / signing secret 不应写入 Markdown 或 Git,请通过安全渠道提供。
@@ -340,8 +340,8 @@ Content-Type: application/json
]
},
"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",
"url": "https://code.xinghanlab.com/api/agent/callbacks/swarm-events",
"signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agent-callback-signing-key",
"subscribed_events": [
"deployment.status_changed",
"task.created",
@@ -416,8 +416,8 @@ Heicode Manager 需要保留普通 sub 和蜂群两套 Runtime 配置。建议
```text
# 普通 sub 敏捷模式
AGNET_RUNTIME_BASE_URL=http://20.212.121.126
AGNET_RUNTIME_CREATE_PATH=/api/swarms
AGENT_RUNTIME_BASE_URL=http://20.212.121.126
AGENT_RUNTIME_CREATE_PATH=/api/swarms
# 蜂群模式
SWARM_RUNTIME_BASE_URL=http://52.139.240.116:8000
@@ -427,7 +427,7 @@ SWARM_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_
SWARM_RUNTIME_SERVICE_TOKEN=<runtime-service-token>
```
如果短期 Manager 还只有一套 `AGNET_RUNTIME_*`,只能临时切换到蜂群 Runtime 做专项联调,不能同时代表普通 sub 和蜂群都在线。
如果短期 Manager 还只有一套 `AGENT_RUNTIME_*`,只能临时切换到蜂群 Runtime 做专项联调,不能同时代表普通 sub 和蜂群都在线。
## 8. 蜂群 MVP 验收口径
@@ -137,13 +137,13 @@ PayPal 接入时有两种做法:
| 文档口径 | 含义 |
|---|---|
| `budget.max_tokens` | 本次 Agent / Agnet 部署允许消耗的 token 上限。 |
| `budget.max_cost_usd` | 本次 Agent / Agnet 部署允许消耗的美元成本上限。 |
| `budget.max_duration_sec` | 本次 Agent / Agnet 部署允许运行的时间上限。 |
| `budget.max_tokens` | 本次 Agent / Agent 部署允许消耗的 token 上限。 |
| `budget.max_cost_usd` | 本次 Agent / Agent 部署允许消耗的美元成本上限。 |
| `budget.max_duration_sec` | 本次 Agent / Agent 部署允许运行的时间上限。 |
| `billing_context.provider = newapi` | 表示模型调用费用应映射到 NewAPI 用户、Token、Group 或 quota。 |
| `agent_runtime` | 表示子 Agent 角色、模型 profile、实例数,不能和 NewAPI 扣费对象混在一起。 |
产品文档明确:子 Agnet 的运行模型属于 Agnet 平台部署配置,不等同于 CodeGW 后台模型供应商配置。NewAPI 负责模型网关、余额、用量、日志和扣费;Agnet 平台负责真实执行和运行态。
产品文档明确:子 Agent 的运行模型属于 Agent 平台部署配置,不等同于 CodeGW 后台模型供应商配置。NewAPI 负责模型网关、余额、用量、日志和扣费;Agent 平台负责真实执行和运行态。
所以当前要分成两类费用:
@@ -378,7 +378,7 @@ Heicode Manager 当前已有余额和订阅计费闭环。PayPal 接入不改变
- Heicode 产品资料包:`docs/product-package/`
- NewAPI / CodeGW 边界:`docs/heicode-runtime-auth-newapi-secret-design.md`
- Agnet 请求契约:`docs/integration/agnet-platform-request-contract.md`
- Agent 请求契约:`docs/integration/agent-platform-request-contract.md`
- PayPal Orders v2:`https://developer.paypal.com/docs/api/orders/v2/`
- PayPal Subscriptions:`https://developer.paypal.com/docs/subscriptions/reference/`
- PayPal Webhook 事件:`https://developer.paypal.com/api/rest/webhooks/event-names`
@@ -8,11 +8,11 @@
| 能力 | 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 |
| 用户态创建 | `POST /api/agent/user/deployments`、`POST /api/swarms` 已可创建 Manager deployment |
| Runtime 创建桥接 | 可通过 `AGENT_RUNTIME_BASE_URL` + `AGENT_RUNTIME_CREATE_PATH` 调用 Runtime |
| Runtime 停止桥接 | 可通过 `AGENT_RUNTIME_STOP_PATH` 停止 Runtime deployment |
| 回调接收 | `POST /api/agent/callbacks/swarm-events` 已支持事件、artifact、approval、timeline 回调 |
| 回调安全 | 支持 `X-Agent-Service-Token` 或 `X-Agent-Signature` HMAC |
| 幂等 | `event_id` / `idempotency_key` 去重 |
| 审批结果回传 | 用户 approve/reject 后,Manager 可调用 Runtime approval decision 接口 |
| 密钥边界 | Manager 只传 `secret_ref` / `credential_ref`,不传长期明文密钥 |
@@ -22,7 +22,7 @@
### 1. 健康检查
```http
GET /api/agnet/health
GET /api/agent/health
```
如果蜂群侧沿用现有 Orchestrator,也可以临时配置为:
@@ -59,7 +59,7 @@ Content-Type: application/json
Manager 当前也可兼容配置:
```text
AGNET_RUNTIME_CREATE_PATH=/api/swarms
AGENT_RUNTIME_CREATE_PATH=/api/swarms
```
请求核心字段:
@@ -97,8 +97,8 @@ AGNET_RUNTIME_CREATE_PATH=/api/swarms
}
],
"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",
"url": "https://code.xinghanlab.com/api/agent/callbacks/swarm-events",
"signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agent-callback-signing-key",
"subscribed_events": [
"deployment.status_changed",
"task.created",
@@ -145,7 +145,7 @@ AGNET_RUNTIME_CREATE_PATH=/api/swarms
默认路径:
```http
POST /api/agnet/deployments/{deployment_id}/stop
POST /api/agent/deployments/{deployment_id}/stop
Authorization: Bearer <service_token>
Content-Type: application/json
```
@@ -181,7 +181,7 @@ POST /api/swarms/{swarm_id}/stop
则 Manager 可通过环境变量配置:
```text
AGNET_RUNTIME_STOP_PATH=/api/swarms/{swarm_id}/stop
AGENT_RUNTIME_STOP_PATH=/api/swarms/{swarm_id}/stop
```
## 三、Runtime 回调 Manager
@@ -189,9 +189,9 @@ AGNET_RUNTIME_STOP_PATH=/api/swarms/{swarm_id}/stop
统一回调入口:
```http
POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
X-Agnet-Service-Token: <callback_token>
X-Agnet-Event-Id: <event_id>
POST https://code.xinghanlab.com/api/agent/callbacks/swarm-events
X-Agent-Service-Token: <callback_token>
X-Agent-Event-Id: <event_id>
X-Correlation-ID: <correlation_id>
Content-Type: application/json
```
@@ -199,8 +199,8 @@ Content-Type: application/json
也支持 HMAC:
```http
X-Agnet-Timestamp: <unix_ms>
X-Agnet-Signature: sha256=<hex>
X-Agent-Timestamp: <unix_ms>
X-Agent-Signature: sha256=<hex>
```
签名内容:
@@ -310,7 +310,7 @@ Content-Type: application/json
Manager 可配置:
```text
AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id}
AGENT_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id}
```
请求:
@@ -329,7 +329,7 @@ AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_
"target_role": "ops",
"risk_level": "high",
"requires_credential": true,
"credential_ref": "lease://agnet/lease_xxx",
"credential_ref": "lease://agent/lease_xxx",
"lease_id": "lease_xxx",
"lease_expires_at": 1770000000000,
"decided_by": "user:22",
@@ -356,7 +356,7 @@ AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_
安全要求:
1. Manager 不会在该请求中发送 `secret_ref`。
2. 如果审批通过且需要凭证,只发送短期 `credential_ref = lease://agnet/...`。
2. 如果审批通过且需要凭证,只发送短期 `credential_ref = lease://agent/...`。
3. Runtime 不得把 `credential_ref`、service token、签名密钥写入日志或 artifact metadata。
4. Runtime 收到 `approved` 后继续原高危动作;收到 `rejected` 后停止该动作并回调 `timeline.updated` 或 `task.failed/blocked`。
@@ -378,7 +378,7 @@ AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_
| 当前入口 | 可用性 | 建议 |
|---|---|---|
| `GET /health` | 可直接作为健康检查 | Manager `AGNET_RUNTIME_HEALTH_PATH=/health` |
| `GET /health` | 可直接作为健康检查 | Manager `AGENT_RUNTIME_HEALTH_PATH=/health` |
| `POST /tasks` | 可临时创建任务 | 只能做兼容桥接,不建议作为最终生产契约 |
| `GET /tasks` | 可查任务 | 后续需要映射到 callback/timeline |
| `GET /agents` | 可查 Agent | 后续需要回调 agent status |
@@ -392,7 +392,7 @@ AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_
最小验收顺序:
1. Manager 调 `GET /api/agnet/health` 或 `/health` 返回 healthy。
1. Manager 调 `GET /api/agent/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 可见。
@@ -1,15 +1,15 @@
# Manager → Agnet 平台接口参数文档
# Manager → Agent 平台接口参数文档
**版本**: v0.3(P1/P5 联调契约)
**生效日期**: 2026-05-03
**状态**: 联调准备;当前仓库提供 Manager 侧最小验证端点,生产 Agnet 平台部署尚未在本文档中宣称完成。
**方向**: Heicode Manager 主动请求 Agnet 平台;Agnet 平台返回部署、日志、监控与审计状态。
**范围**: 创建/停止子 Agent 部署、查询部署、获取事件/日志/监控快照、解析 SK 快照、查询审计日志,以及 Agnet 辅助 NewAPI 重建/部署的参数约定。
**状态**: 联调准备;当前仓库提供 Manager 侧最小验证端点,生产 Agent 平台部署尚未在本文档中宣称完成。
**方向**: Heicode Manager 主动请求 Agent 平台;Agent 平台返回部署、日志、监控与审计状态。
**范围**: 创建/停止子 Agent 部署、查询部署、获取事件/日志/监控快照、解析 SK 快照、查询审计日志,以及 Agent 辅助 NewAPI 重建/部署的参数约定。
> 2026-05-04 边界修正:Manager 当前不把 `tenant/project` 作为产品、认证或扣费主轴。新请求应使用 `user_context.user_id`、`user_context.channel_id`、`resource_grants[].binding_scope`、`billing_context(newapi)` 和 `agent_runtime(agnet)`。本文中仍出现的 `tenant_id/project_id` 只表示旧字段兼容或历史接口命名,不应作为新功能设计依据。
> 2026-05-04 边界修正:Manager 当前不把 `tenant/project` 作为产品、认证或扣费主轴。新请求应使用 `user_context.user_id`、`user_context.channel_id`、`resource_grants[].binding_scope`、`billing_context(newapi)` 和 `agent_runtime(agent)`。本文中仍出现的 `tenant_id/project_id` 只表示旧字段兼容或历史接口命名,不应作为新功能设计依据。
**安全红线**: 请求体只允许传资源元数据、权限范围与 `secret_ref`/环境变量名;不得传明文密码、Token、私钥、连接串或云访问密钥。
> 本文档描述 Manager 对 Agnet 平台的出站集成契约。当前仓库中 `/api/agnet/*` 是 Manager 侧最小控制面/模拟端点,用于校验同一套 payload 结构;生产接入时,Manager 应将下列请求发送到 Agnet 平台网关。
> 本文档描述 Manager 对 Agent 平台的出站集成契约。当前仓库中 `/api/agent/*` 是 Manager 侧最小控制面/模拟端点,用于校验同一套 payload 结构;生产接入时,Manager 应将下列请求发送到 Agent 平台网关。
## 0. 概述
@@ -17,18 +17,18 @@
| 接口 | 用途 | 当前性质 |
|---|---|---|
| `POST /api/agnet/deployments` | 创建子 Agent/运维任务部署,含 NewAPI 重建/部署场景 | 必需 |
| `GET /api/agnet/deployments` | 查询部署列表 | 必需 |
| `GET /api/agnet/deployments/{deployment_id}` | 查询单个部署详情 | 必需 |
| `POST /api/agnet/deployments/{deployment_id}/stop` | 停止部署或取消排队任务 | 必需 |
| `GET /api/agnet/deployments/{deployment_id}/logs` | 拉取部署日志 | 必需 |
| `GET /api/agnet/deployments/{deployment_id}/logs/stream` | 实时日志 SSE | 可选 |
| `GET /api/agnet/projects/{project_id}/dashboard-snapshot` | 资源作用域监控快照;路径名保留旧兼容,参数值按 `binding_scope` 解释 | 必需 |
| `GET /api/agnet/deployments/{deployment_id}/metrics` | 单部署指标序列 | 建议 |
| `GET /api/agnet/deployments/{deployment_id}/events` | 部署事件 | 必需 |
| `GET /api/agnet/audit-logs` | 审计日志 | 必需 |
| `POST /api/agnet/sk-snapshots/resolve` | 触发 SK 快照解析 | 必需 |
| `GET /api/agnet/deployments/{deployment_id}/sk-snapshots` | 查询 SK 快照 | 必需 |
| `POST /api/agent/deployments` | 创建子 Agent/运维任务部署,含 NewAPI 重建/部署场景 | 必需 |
| `GET /api/agent/deployments` | 查询部署列表 | 必需 |
| `GET /api/agent/deployments/{deployment_id}` | 查询单个部署详情 | 必需 |
| `POST /api/agent/deployments/{deployment_id}/stop` | 停止部署或取消排队任务 | 必需 |
| `GET /api/agent/deployments/{deployment_id}/logs` | 拉取部署日志 | 必需 |
| `GET /api/agent/deployments/{deployment_id}/logs/stream` | 实时日志 SSE | 可选 |
| `GET /api/agent/projects/{project_id}/dashboard-snapshot` | 资源作用域监控快照;路径名保留旧兼容,参数值按 `binding_scope` 解释 | 必需 |
| `GET /api/agent/deployments/{deployment_id}/metrics` | 单部署指标序列 | 建议 |
| `GET /api/agent/deployments/{deployment_id}/events` | 部署事件 | 必需 |
| `GET /api/agent/audit-logs` | 审计日志 | 必需 |
| `POST /api/agent/sk-snapshots/resolve` | 触发 SK 快照解析 | 必需 |
| `GET /api/agent/deployments/{deployment_id}/sk-snapshots` | 查询 SK 快照 | 必需 |
> 不在本文档范围:真实 Secret Store 写入、生产 SSH 登录、云账号授权回调、NewAPI 管理后台开放。生产部署动作只有实际执行并通过日志/监控/审计验证后,才能在报告中标记为“已部署”。
@@ -41,20 +41,20 @@
由部署环境配置,不写入仓库。例如:
```text
AGNET_PLATFORM_BASE_URL=https://agnet-platform.example.com
AGENT_PLATFORM_BASE_URL=https://agent-platform.example.com
```
联调环境建议使用独立域名或内网网关,示例不得包含真实凭据:
```text
AGNET_PLATFORM_BASE_URL=https://staging-agnet.example.com
MANAGER_SERVICE_TOKEN_SECRET_REF=azkv://heicode-kv.vault.azure.net/secrets/manager-service-agnet-platform-service-token
AGENT_PLATFORM_BASE_URL=https://staging-agent.example.com
MANAGER_SERVICE_TOKEN_SECRET_REF=azkv://heicode-kv.vault.azure.net/secrets/manager-service-agent-platform-service-token
```
完整路径示例:
```http
POST https://agnet-platform.example.com/api/agnet/deployments
POST https://agent-platform.example.com/api/agent/deployments
```
### 1.2 通用 Header
@@ -133,7 +133,7 @@ POST https://agnet-platform.example.com/api/agnet/deployments
### 2.1 Endpoint
```http
POST /api/agnet/deployments
POST /api/agent/deployments
```
### 2.2 请求体
@@ -164,11 +164,11 @@ POST /api/agnet/deployments
"quota_ref": "newapi_token_or_group_quota_ref"
},
"agent_runtime": {
"platform": "agnet",
"platform": "agent",
"agents": [
{
"role": "builder",
"model_ref": "agnet_model_profile_builder",
"model_ref": "agent_model_profile_builder",
"instance_count": 1
}
]
@@ -200,7 +200,7 @@ POST /api/agnet/deployments
],
"runtime_execution": {
"profile_id": "aks-codex-standard",
"cloud_principal_refs": ["principal://users/user_123/agnet-runtime"],
"cloud_principal_refs": ["principal://users/user_123/agent-runtime"],
"network_policy_ref": "netpol://bindings/repo_default/restricted-egress"
},
"sk_access_policy": {
@@ -272,15 +272,15 @@ POST /api/agnet/deployments
| 字段 | 类型 | 必填 | 约束/说明 |
|---|---|---:|---|
| `intent_id` | string | 是 | Manager 侧意图 ID,用于幂等、审计和追踪。 |
| `template_hint` | string | 是 | Agnet 平台选择编排模板的提示,如 `manager-resource-binding`。 |
| `template_hint` | string | 是 | Agent 平台选择编排模板的提示,如 `manager-resource-binding`。 |
| `objective` | string | 是 | 本次部署目标,应是自然语言但不能含密钥。 |
| `risk_level` | enum | 是 | `low` / `medium` / `high`。高风险必须携带客户端审批证据;缺失或不匹配时只能只读或拒绝执行。 |
| `budget.max_tokens` | int | 是 | 当前策略上限建议不超过 `500000`。 |
| `budget.max_cost_usd` | number | 是 | 当前策略上限建议不超过 `200`。 |
| `budget.max_duration_sec` | int | 是 | 当前策略上限建议不超过 `86400`。 |
| `user_context` | object | 建议 | 登录用户上下文,优先使用 Heicode/Agnet 登录返回的 `user.id`/`channelId`。 |
| `billing_context` | object | 条件 | NewAPI 扣费上下文;只表达 user/token/group/quota 映射,不表达子 Agnet 模型或实例数。 |
| `agent_runtime` | object | 条件 | Agnet 平台运行时上下文;表达子 Agnet 角色、模型 profile 和实例数,不承载 NewAPI key 或扣费对象。 |
| `user_context` | object | 建议 | 登录用户上下文,优先使用 Heicode/Agent 登录返回的 `user.id`/`channelId`。 |
| `billing_context` | object | 条件 | NewAPI 扣费上下文;只表达 user/token/group/quota 映射,不表达子 Agent 模型或实例数。 |
| `agent_runtime` | object | 条件 | Agent 平台运行时上下文;表达子 Agent 角色、模型 profile 和实例数,不承载 NewAPI key 或扣费对象。 |
| `constraints.allowed_model_ids` | string[] | 否 | agent 的 `default_model_id` 如填写,必须在此列表内。 |
| `metadata.tenant_id` | string | 否 | 旧兼容字段;新实现不得作为产品租户边界。 |
| `metadata.project_id` | string | 否 | 旧兼容字段;新实现不得作为项目账本边界。 |
@@ -297,10 +297,10 @@ POST /api/agnet/deployments
| `billing_context.newapi_user_ref` | string | 否 | NewAPI 用户映射引用,不是 NewAPI key。 |
| `billing_context.newapi_group` | string | 否 | NewAPI Group 映射,用于额度或策略选择。 |
| `billing_context.quota_ref` | string | 否 | Token 或 Group 额度引用,不得包含真实 Token 原文。 |
| `agent_runtime.platform` | enum | 条件 | 当前只允许 `agnet`。 |
| `agent_runtime.platform` | enum | 条件 | 当前只允许 `agent`。 |
| `agent_runtime.agents[].role` | string | 条件 | 必须匹配 `agents[].role_template`。 |
| `agent_runtime.agents[].model_ref` | string | 条件 | Agnet 平台模型 profile 引用;不是 NewAPI 扣费字段。 |
| `agent_runtime.agents[].instance_count` | int | 条件 | 子 Agnet 实例数量,必须大于 0。 |
| `agent_runtime.agents[].model_ref` | string | 条件 | Agent 平台模型 profile 引用;不是 NewAPI 扣费字段。 |
| `agent_runtime.agents[].instance_count` | int | 条件 | 子 Agent 实例数量,必须大于 0。 |
#### agents[]
@@ -330,7 +330,7 @@ POST /api/agnet/deployments
| 字段 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `profile_id` | string | 条件必填 | Agnet 平台运行规格,如 AKS profile。 |
| `profile_id` | string | 条件必填 | Agent 平台运行规格,如 AKS profile。 |
| `cloud_principal_refs` | string[] | 否 | 运行身份引用,不是明文凭据。 |
| `network_policy_ref` | string | 否 | 网络策略引用,用于限制出站/入站。 |
@@ -354,9 +354,9 @@ POST /api/agnet/deployments
| `secret_ref` | string | 条件必填 | `git`、`sk`、`cloud_account`、`cloud_resource` 必填;`project_doc` 可为空。 |
| `audit` | object<string,string> | 否 | 审计上下文;不得含密钥字段。 |
### 2.4 典型场景:Agnet 辅助 NewAPI 重建/部署
### 2.4 典型场景:Agent 辅助 NewAPI 重建/部署
当 Manager 需要让 Agnet 平台协助重建或部署 NewAPI 时,仍使用 `POST /api/agnet/deployments`,但必须把任务表达为受控运维部署,不得把 VM、PostgreSQL、Redis、NewAPI key 等真实凭据写入请求体。
当 Manager 需要让 Agent 平台协助重建或部署 NewAPI 时,仍使用 `POST /api/agent/deployments`,但必须把任务表达为受控运维部署,不得把 VM、PostgreSQL、Redis、NewAPI key 等真实凭据写入请求体。
请求体示例:
@@ -394,7 +394,7 @@ POST /api/agnet/deployments
"default_model_id": "gpt-5.4",
"runtime_execution": {
"profile_id": "aks-codex-ops",
"cloud_principal_refs": ["principal://users/user_123/agnet-ops"],
"cloud_principal_refs": ["principal://users/user_123/agent-ops"],
"network_policy_ref": "netpol://bindings/newapi-prod/ops-egress"
},
"resource_grants": [
@@ -458,7 +458,7 @@ POST /api/agnet/deployments
}
```
Agnet 平台返回的部署详情、日志、监控和审计中应至少能证明:构建版本/commit、服务重启结果、健康检查结果、资源使用情况、失败回滚状态。未执行真实 SSH/生产动作时,只能返回 `phase=planned` 或 `phase=pending_approval`。
Agent 平台返回的部署详情、日志、监控和审计中应至少能证明:构建版本/commit、服务重启结果、健康检查结果、资源使用情况、失败回滚状态。未执行真实 SSH/生产动作时,只能返回 `phase=planned` 或 `phase=pending_approval`。
NewAPI 重建/部署的完成判定必须同时满足:
@@ -494,7 +494,7 @@ NewAPI 重建/部署的完成判定必须同时满足:
### 3.1 查询部署列表
```http
GET /api/agnet/deployments?user_id=user_123&binding_scope=repo_default
GET /api/agent/deployments?user_id=user_123&binding_scope=repo_default
```
返回:
@@ -520,15 +520,15 @@ GET /api/agnet/deployments?user_id=user_123&binding_scope=repo_default
### 3.2 查询单个部署
```http
GET /api/agnet/deployments/{deployment_id}
GET /api/agent/deployments/{deployment_id}
```
返回应包含部署状态、phase、agent_instances、最近错误、资源授权摘要和预算消耗摘要。Agnet 平台返回时必须对 `secret_ref` 以外的凭据信息做脱敏;原则上不返回任何明文凭据。
返回应包含部署状态、phase、agent_instances、最近错误、资源授权摘要和预算消耗摘要。Agent 平台返回时必须对 `secret_ref` 以外的凭据信息做脱敏;原则上不返回任何明文凭据。
### 3.3 停止部署
```http
POST /api/agnet/deployments/{deployment_id}/stop
POST /api/agent/deployments/{deployment_id}/stop
```
请求体可为空;如需原因可扩展:
@@ -565,12 +565,12 @@ POST /api/agnet/deployments/{deployment_id}/stop
---
## 4. 日志接口(Manager 拉取 Agnet 平台)
## 4. 日志接口(Manager 拉取 Agent 平台)
### 4.1 获取部署日志
```http
GET /api/agnet/deployments/{deployment_id}/logs?agent_instance_id=agi_abc123&stream=stdout&since=2026-05-02T00:00:00Z&limit=200&cursor=cur_001
GET /api/agent/deployments/{deployment_id}/logs?agent_instance_id=agi_abc123&stream=stdout&since=2026-05-02T00:00:00Z&limit=200&cursor=cur_001
```
Query:
@@ -609,14 +609,14 @@ Query:
日志要求:
- Agnet 平台必须在返回前完成密钥脱敏。
- Agent 平台必须在返回前完成密钥脱敏。
- `message` 不得包含密码、Token、私钥、连接串、云访问密钥。
- Manager 只保存必要摘要和审计索引;长日志建议落对象存储并设置保留期。
### 4.2 实时日志流(可选)
```http
GET /api/agnet/deployments/{deployment_id}/logs/stream?agent_instance_id=agi_abc123
GET /api/agent/deployments/{deployment_id}/logs/stream?agent_instance_id=agi_abc123
Accept: text/event-stream
```
@@ -646,12 +646,12 @@ SSE 事件类型:
---
## 5. 监控接口(Manager 拉取 Agnet 平台)
## 5. 监控接口(Manager 拉取 Agent 平台)
### 5.1 项目监控快照
```http
GET /api/agnet/projects/{binding_scope}/dashboard-snapshot?window=1h
GET /api/agent/projects/{binding_scope}/dashboard-snapshot?window=1h
```
响应:
@@ -689,7 +689,7 @@ GET /api/agnet/projects/{binding_scope}/dashboard-snapshot?window=1h
### 5.2 单部署监控快照(建议平台实现)
```http
GET /api/agnet/deployments/{deployment_id}/metrics?window=15m&step=60s
GET /api/agent/deployments/{deployment_id}/metrics?window=15m&step=60s
```
响应:
@@ -725,7 +725,7 @@ GET /api/agnet/deployments/{deployment_id}/metrics?window=15m&step=60s
### 6.1 部署事件
```http
GET /api/agnet/deployments/{deployment_id}/events?since=2026-05-02T00:00:00Z&limit=200&cursor=cur_001
GET /api/agent/deployments/{deployment_id}/events?since=2026-05-02T00:00:00Z&limit=200&cursor=cur_001
```
响应:
@@ -768,7 +768,7 @@ GET /api/agnet/deployments/{deployment_id}/events?since=2026-05-02T00:00:00Z&lim
### 6.2 审计日志
```http
GET /api/agnet/audit-logs?user_id=user_123&binding_scope=repo_default&actor=agnet_control_plane&action=deployment.accepted&since=2026-05-02T00:00:00Z&limit=200&cursor=cur_001
GET /api/agent/audit-logs?user_id=user_123&binding_scope=repo_default&actor=agent_control_plane&action=deployment.accepted&since=2026-05-02T00:00:00Z&limit=200&cursor=cur_001
```
响应:
@@ -780,7 +780,7 @@ GET /api/agnet/audit-logs?user_id=user_123&binding_scope=repo_default&actor=agne
"items": [
{
"audit_id": "aud_001",
"actor": "agnet_control_plane",
"actor": "agent_control_plane",
"action": "deployment.accepted",
"resource": "dep_abc123",
"user_id": "user_123",
@@ -805,7 +805,7 @@ GET /api/agnet/audit-logs?user_id=user_123&binding_scope=repo_default&actor=agne
### 7.1 触发解析/刷新
```http
POST /api/agnet/sk-snapshots/resolve
POST /api/agent/sk-snapshots/resolve
```
请求:
@@ -842,7 +842,7 @@ POST /api/agnet/sk-snapshots/resolve
### 7.2 查询部署 SK 快照
```http
GET /api/agnet/deployments/{deployment_id}/sk-snapshots
GET /api/agent/deployments/{deployment_id}/sk-snapshots
```
Query:
@@ -893,17 +893,17 @@ Query:
## 8. 安全校验清单
Manager 发给 Agnet 平台前必须执行:
Manager 发给 Agent 平台前必须执行:
1. `user_id`、`binding_scope`、`target_role` 与部署计划一致。
2. 凭据型资源只传 `secret_ref`,不传明文凭据。
3. `metadata`、`constraints`、`audit` 的 key 中不得出现 `password`、`token`、`secret`、`private_key`、`access_key`、`credential` 等敏感词。
4. `repo_url` 不得包含用户名、密码或访问 Token。
5. `permission_scope` 使用最小权限;生产写操作必须携带客户端审批记录,Agnet 平台不得自行补批。
6. 高风险操作(生产部署、云资源修改、删除、扩容)必须设置 `risk_level=high`,Agnet 平台执行前只校验客户端审批证据。
5. `permission_scope` 使用最小权限;生产写操作必须携带客户端审批记录,Agent 平台不得自行补批。
6. 高风险操作(生产部署、云资源修改、删除、扩容)必须设置 `risk_level=high`,Agent 平台执行前只校验客户端审批证据。
7. 所有日志/事件/审计返回给 Manager 前必须脱敏。
Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Agnet 平台只能在执行前校验以下字段和策略是否一致:
Agent 平台不承担高危操作审批主体。审批只发生在客户端;Agent 平台只能在执行前校验以下字段和策略是否一致:
| 校验项 | 要求 |
|---|---|
@@ -914,14 +914,14 @@ Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Ag
| `risk_level` | 高危资源写入、生产部署、云资源修改、删除和扩容必须为 `high`。 |
| 策略 | 平台 policy、Key Vault 访问策略、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 |
任一校验不通过时,Agnet 平台应返回 `POLICY_REJECTED` 或 `FORBIDDEN_SCOPE`,不得发起额外批准流程。
任一校验不通过时,Agent 平台应返回 `POLICY_REJECTED` 或 `FORBIDDEN_SCOPE`,不得发起额外批准流程。
### 8.1 字段级约束速查
| 对象/接口 | 必填最小集合 | 禁止内容 |
|---|---|---|
| `orchestration_plan` | `intent_id`、`template_hint`、`objective`、`risk_level`、`budget`、`user_context`、`billing_context`、`agent_runtime`、`metadata.correlation_id`、`agents[]` | 密钥、连接串、真实主机登录密码、NewAPI key 原文。 |
| `agents[]` | `role_template`、`goal` | 让子 Agent 绕过 Manager/Agnet 审计的指令。 |
| `agents[]` | `role_template`、`goal` | 让子 Agent 绕过 Manager/Agent 审计的指令。 |
| `runtime_execution` | 任一字段存在时 `profile_id` 必填 | 明文 kubeconfig、SSH key、云访问密钥。 |
| `resource_grants[]` | `grant_id`、`resource_id`、`resource_type`、`user_id`、`binding_scope`、`target_role`、`target_agent_ref`、`permission_scope`、`status` | 明文 `password`、`token`、`private_key`、`access_key`、`credential`、数据库 DSN。 |
| 日志/事件/审计返回 | `request_id` 或 `correlation_id`,以及发生时间 | 未脱敏命令行、环境变量 dump、密钥片段。 |
@@ -934,7 +934,7 @@ Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Ag
- `risk_level=high` 的生产运维任务包含 `approval_id` 和回滚引用。
- 日志、事件、监控、审计接口都能通过 `correlation_id` 串联。
- NewAPI 重建/部署只在实际执行并通过健康检查后标记为已部署;未执行时状态只能是 `planned`、`pending_approval`、`accepted` 或 `running`。
- Manager 本地 `/api/agnet/*` 占位端点通过 payload 校验不等于生产 Agnet 平台已上线。
- Manager 本地 `/api/agent/*` 占位端点通过 payload 校验不等于生产 Agent 平台已上线。
---
@@ -944,18 +944,18 @@ Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Ag
| Manager 路由 | 用途 |
|---|---|
| `POST /api/agnet/deployments` | 校验并接受 orchestration_plan。 |
| `GET /api/agnet/deployments` | 按 user/binding scope 查询部署。 |
| `GET /api/agnet/deployments/:deployment_id` | 查询部署详情。 |
| `POST /api/agnet/deployments/:deployment_id/stop` | 停止部署。 |
| `GET /api/agnet/deployments/:deployment_id/logs` | 查询脱敏日志占位/联调日志。 |
| `GET /api/agnet/deployments/:deployment_id/metrics` | 查询单部署指标占位/联调指标。 |
| `GET /api/agnet/deployments/:deployment_id/events` | 查询事件。 |
| `POST /api/agnet/sk-snapshots/resolve` | 解析 SK 快照。 |
| `GET /api/agnet/deployments/:deployment_id/sk-snapshots` | 查询 SK 快照。 |
| `GET /api/agnet/projects/:project_id/dashboard-snapshot` | 资源作用域监控快照;路径名保留旧兼容。 |
| `GET /api/agnet/audit-logs` | 审计日志。 |
| `POST /api/agent/deployments` | 校验并接受 orchestration_plan。 |
| `GET /api/agent/deployments` | 按 user/binding scope 查询部署。 |
| `GET /api/agent/deployments/:deployment_id` | 查询部署详情。 |
| `POST /api/agent/deployments/:deployment_id/stop` | 停止部署。 |
| `GET /api/agent/deployments/:deployment_id/logs` | 查询脱敏日志占位/联调日志。 |
| `GET /api/agent/deployments/:deployment_id/metrics` | 查询单部署指标占位/联调指标。 |
| `GET /api/agent/deployments/:deployment_id/events` | 查询事件。 |
| `POST /api/agent/sk-snapshots/resolve` | 解析 SK 快照。 |
| `GET /api/agent/deployments/:deployment_id/sk-snapshots` | 查询 SK 快照。 |
| `GET /api/agent/projects/:project_id/dashboard-snapshot` | 资源作用域监控快照;路径名保留旧兼容。 |
| `GET /api/agent/audit-logs` | 审计日志。 |
生产对接时,Manager 应把相同契约的请求发送给 Agnet 平台;本地 Manager 端点仅作为最小验证与控制面占位,不代表所有日志/监控平台能力已完整实现。
生产对接时,Manager 应把相同契约的请求发送给 Agent 平台;本地 Manager 端点仅作为最小验证与控制面占位,不代表所有日志/监控平台能力已完整实现。
当前本地 `logs` 与 `metrics` 端点只返回脱敏占位/联调数据,用于验证 Manager ↔ Agnet payload、路由和验收流程。生产级实时日志流 `GET /api/agnet/deployments/{deployment_id}/logs/stream` 仍属于 Agnet 平台能力;Manager 不得把“本地占位通过”误报为“生产日志/监控已上线”。
当前本地 `logs` 与 `metrics` 端点只返回脱敏占位/联调数据,用于验证 Manager ↔ Agent payload、路由和验收流程。生产级实时日志流 `GET /api/agent/deployments/{deployment_id}/logs/stream` 仍属于 Agent 平台能力;Manager 不得把“本地占位通过”误报为“生产日志/监控已上线”。
@@ -45,7 +45,7 @@ Manager 生产地址:`https://code.xinghanlab.com`
重要修正:
1. 桌面客户端不要再使用 `agnet-model-builder`、`agnet-model-reviewer`、`agnet-model-product` 这类占位模型名。生产 NewAPI 没有这些模型,会返回 `No available channel for model ...`。
1. 桌面客户端不要再使用 `agent-model-builder`、`agent-model-reviewer`、`agent-model-product` 这类占位模型名。生产 NewAPI 没有这些模型,会返回 `No available channel for model ...`。
2. 普通 sub 当前优先建议使用生产已复核通过的 `gpt-5.4`。`claude-sonnet-4-6` 仍可作为 NewAPI 模型存在,但当前普通 sub Runtime 若按普通 `/v1/chat/completions` 方式调用 Claude Code 类模型,可能返回上游 400;客户端不要把该错误误判为加密或 Manager 创建失败。
3. `orchestration_plan.metadata.correlation_id` 是创建 deployment 的必填字段,必须放在 `orchestration_plan` 内,不是顶层 `metadata`。
4. 成功态不能只看 `status=completed`。客户端还应确认 `runtime_state=completed`、`artifacts.length > 0`、artifact 不是失败摘要、`tokens_used > 0`,并优先展示可下载的业务交付物。
@@ -56,7 +56,7 @@ Manager 生产地址:`https://code.xinghanlab.com`
桌面客户端负责用户主体验:输入想法、回答追问、持续推进任务、查看子环节反馈、处理高危审批、接收交付结果。
Manager 负责辅助控制面:任务草稿桥接、资源/权限、Agnet deployment、状态/timeline/artifact/SK 查询、审批记录和短期凭证 lease。
Manager 负责辅助控制面:任务草稿桥接、资源/权限、Agent deployment、状态/timeline/artifact/SK 查询、审批记录和短期凭证 lease。
本文只描述普通 sub 敏捷流程,不包含蜂群 task graph、claim、heartbeat、handoff 等蜂群模式能力。
@@ -67,7 +67,7 @@ Manager 负责辅助控制面:任务草稿桥接、资源/权限、Agnet deplo
| API 前缀 | 用途 | 认证 |
|---|---|---|
| `/api/heicode-auth/api/user/tasks/*` | HeicodeTask 任务编排代理,创建任务、追问、查询任务 | V2 加密 body + `Authorization: Bearer <heicode_access_token>` |
| `/api/agnet/user/*` | Manager 用户态 Agnet 控制面,deployment、timeline、artifact、审批 | V2 加密 body;未加密 Web 控制台请求继续使用 Manager session + `New-Api-User` |
| `/api/agent/user/*` | Manager 用户态 Agent 控制面,deployment、timeline、artifact、审批 | V2 加密 body;未加密 Web 控制台请求继续使用 Manager session + `New-Api-User` |
| `/api/swarms` | 蜂群模式创建入口 / Runtime adapter 入口 | V2 加密 body;未加密 Web 控制台请求继续使用 Manager session + `New-Api-User` |
| `/api/user/self` | 查询当前 Manager 用户 | Manager 登录 session cookie |
@@ -79,7 +79,7 @@ Manager 负责辅助控制面:任务草稿桥接、资源/权限、Agnet deplo
| API 前缀 | V2 加密 body | 说明 |
|---|---|---|
| `/api/agnet/user/*` | 支持 | Manager 解密并校验设备签名后,按当前设备对应用户执行 |
| `/api/agent/user/*` | 支持 | Manager 解密并校验设备签名后,按当前设备对应用户执行 |
| `/api/swarms` | 支持 | Manager 解密并校验设备签名后,按当前设备对应用户创建蜂群/Runtime adapter deployment |
| `/api/heicode-auth/*` | 支持 | Manager 解密并校验设备签名后,把明文 body 代理给上游 HeicodeTask 服务;仍需携带 `heicode_access_token` |
| 浏览器后台普通页面请求 | 兼容未加密 JSON | 不影响现有 Manager Web 控制台 |
@@ -127,14 +127,14 @@ Authorization: Bearer <heicode_access_token>
客户端实现要求:
1. sub 流程调用不要新增一套加密协议,直接复用模型调用的 `encryptedFetch` / V2 设备签名实现。
2. `aad` 和 `canonical` 中的 `path_with_query` 必须是 Manager 实际收到的 path,例如 `/api/agnet/user/tasks/task-1/deployment-draft`,不能把 origin 写进去。
2. `aad` 和 `canonical` 中的 `path_with_query` 必须是 Manager 实际收到的 path,例如 `/api/agent/user/tasks/task-1/deployment-draft`,不能把 origin 写进去。
3. 加密前的 plaintext 必须是最终 JSON body;签名里的 `sha256_hex(plaintext_body)` 必须和该 JSON 字节完全一致。
4. 每次请求必须使用新的 `X-Heicode-Nonce` 和新的 X25519 ephemeral key。
5. V2 请求失败时优先读取 `X-Heicode-Auth-Error` 和 `X-Heicode-Server-Time`,用于提示设备未配对、时间漂移、nonce 重放、签名错误或解密失败。
### 2.3 Manager 用户态 Header
未加密 Web 控制台请求调用 `/api/agnet/user/*` 或 `/api/swarms` 时必须带:
未加密 Web 控制台请求调用 `/api/agent/user/*` 或 `/api/swarms` 时必须带:
```http
Cookie: session=<manager-session>
@@ -145,7 +145,7 @@ Accept: application/json
`New-Api-User` 必须等于当前登录用户 ID,否则会返回未授权。
使用 V2 加密 body 时,`/api/agnet/user/*` 和 `/api/swarms` 不依赖浏览器 session cookie,也不需要 `New-Api-User`;Manager 会从设备绑定 token 中解析用户身份。为兼容当前 Web 控制台,未加密请求仍按 session cookie + `New-Api-User` 处理。
使用 V2 加密 body 时,`/api/agent/user/*` 和 `/api/swarms` 不依赖浏览器 session cookie,也不需要 `New-Api-User`;Manager 会从设备绑定 token 中解析用户身份。为兼容当前 Web 控制台,未加密请求仍按 session cookie + `New-Api-User` 处理。
### 2.4 蜂群入口加密边界
@@ -153,8 +153,8 @@ Accept: application/json
| 接口 | 所属模式 | 加密要求 |
|---|---|---|
| `POST /api/agnet/user/tasks/{task_id}/deployment-draft` | 普通 sub 敏捷 | V2 body 加密 |
| `POST /api/agnet/user/deployments` | 普通 sub 敏捷 | V2 body 加密 |
| `POST /api/agent/user/tasks/{task_id}/deployment-draft` | 普通 sub 敏捷 | V2 body 加密 |
| `POST /api/agent/user/deployments` | 普通 sub 敏捷 | V2 body 加密 |
| `POST /api/swarms` | 蜂群模式 | V2 body 加密 |
注意:本文后续章节仍只描述普通 sub 敏捷主流程;蜂群 task graph、claim、heartbeat、handoff、approval decision 等字段以单独蜂群对接文档为准。
@@ -190,8 +190,8 @@ Accept: application/json
1. 桌面端确认 Manager 登录态,获取 /api/user/self
2. 创建 HeicodeTask:POST /api/heicode-auth/api/user/tasks/intent
3. 如果 status=configuring,回答追问:POST /api/heicode-auth/api/user/tasks/{task_id}/answer
4. 当任务卡生成后,创建 deployment draft:POST /api/agnet/user/tasks/{task_id}/deployment-draft
5. 创建 Manager deployment:POST /api/agnet/user/deployments
4. 当任务卡生成后,创建 deployment draft:POST /api/agent/user/tasks/{task_id}/deployment-draft
5. 创建 Manager deployment:POST /api/agent/user/deployments
6. 轮询 deployment detail / events / timeline
7. 查询 runtime-diagnostics,区分 callback 已到、Runtime 状态、失败 Agent 和兜底摘要 artifact
8. 查询 artifacts 列表;如果需要完整文件,调用 artifact content 代理接口下载
@@ -205,14 +205,14 @@ Accept: application/json
客户端展示普通 sub 结果时,不能只看 deployment `status=completed`。Manager 已提供只读诊断接口,用来识别 Runtime 是否真的完成、是否有 failed agent、是否只返回兜底摘要 artifact。
```http
GET /api/agnet/user/deployments/{deployment_id}/runtime-diagnostics
GET /api/agent/user/deployments/{deployment_id}/runtime-diagnostics
```
返回核心字段:
| 字段 | 说明 |
|---|---|
| `runtime_mode` | `agnet` 表示普通 sub Runtime;`swarm` 表示蜂群 Runtime |
| `runtime_mode` | `agent` 表示普通 sub Runtime;`swarm` 表示蜂群 Runtime |
| `runtime_swarm_id` / `runtime_deployment_id` | Manager 保存的 Runtime 映射 |
| `data_source` | 当前诊断来源,正常为 `runtime_status` |
| `status` / `phase` | Runtime 直接返回的状态和阶段 |
@@ -235,7 +235,7 @@ GET /api/agnet/user/deployments/{deployment_id}/runtime-diagnostics
1. deployment detail / timeline / artifacts 仍按原接口展示。
2. 若 `warnings` 包含 `runtime_summary_artifact_only`,需要提示“当前没有最终交付产物,请查看运行日志/等待 Runtime 修复”。
3. 若 `runtime_mode=agnet`,按普通 sub 敏捷展示;若 `runtime_mode=swarm`,按蜂群模式展示任务图/Agent 编队,不能混用两套文案。
3. 若 `runtime_mode=agent`,按普通 sub 敏捷展示;若 `runtime_mode=swarm`,按蜂群模式展示任务图/Agent 编队,不能混用两套文案。
4. 该接口只读,失败时不应中断已有 timeline/artifact 展示。
### 3.2 Artifact 完整内容下载
@@ -245,13 +245,13 @@ GET /api/agnet/user/deployments/{deployment_id}/runtime-diagnostics
列表:
```http
GET /api/agnet/user/deployments/{deployment_id}/artifacts
GET /api/agent/user/deployments/{deployment_id}/artifacts
```
下载:
```http
GET /api/agnet/user/deployments/{deployment_id}/artifacts/{artifact_id}/content
GET /api/agent/user/deployments/{deployment_id}/artifacts/{artifact_id}/content
```
Manager 行为:
@@ -339,26 +339,26 @@ function classifyArtifact(item: Artifact, content: string, contentType?: string)
| Manager 版本 | `1.4.19` |
| Agent Manager Runtime | `http://20.212.121.126` |
| Runtime health | `healthy` |
| Manager callback | `https://code.xinghanlab.com/api/agnet/callbacks/swarm-events` |
| Manager callback | `https://code.xinghanlab.com/api/agent/callbacks/swarm-events` |
已验证成功的链路:
```text
Manager 登录
-> /api/user/self
-> /api/agnet/runtime/health
-> /api/agnet/user/tasks/{task_id}/deployment-draft
-> /api/agnet/user/deployments
-> /api/agent/runtime/health
-> /api/agent/user/tasks/{task_id}/deployment-draft
-> /api/agent/user/deployments
-> Manager 调 Agent Manager Runtime create
-> Runtime 自动 callback 到 Manager
-> /api/agnet/user/deployments/{deployment_id}
-> /api/agnet/user/deployments/{deployment_id}/metrics
-> /api/agnet/user/deployments/{deployment_id}/events
-> /api/agnet/user/deployments/{deployment_id}/logs
-> /api/agnet/user/deployments/{deployment_id}/artifacts
-> /api/agnet/user/deployments/{deployment_id}/sk-snapshots
-> /api/agnet/user/deployments/{deployment_id}/timeline
-> /api/agnet/user/deployments/{deployment_id}/stop
-> /api/agent/user/deployments/{deployment_id}
-> /api/agent/user/deployments/{deployment_id}/metrics
-> /api/agent/user/deployments/{deployment_id}/events
-> /api/agent/user/deployments/{deployment_id}/logs
-> /api/agent/user/deployments/{deployment_id}/artifacts
-> /api/agent/user/deployments/{deployment_id}/sk-snapshots
-> /api/agent/user/deployments/{deployment_id}/timeline
-> /api/agent/user/deployments/{deployment_id}/stop
```
最新生产烟测 ID:
@@ -431,7 +431,7 @@ Manager 登录
客户端处理:
- 保存 `data.id`。
- 后续 `/api/agnet/user/*` 请求带 `New-Api-User: <id>`。
- 后续 `/api/agent/user/*` 请求带 `New-Api-User: <id>`。
## 5. HeicodeTask 任务编排
@@ -594,11 +594,11 @@ Authorization: Bearer <heicode_access_token>
- `status=awaiting_approval`:轮询审批接口。
- `status=completed/failed/paused`:停止高频轮询。
## 6. 从任务生成 Agnet Deployment Draft
## 6. 从任务生成 Agent Deployment Draft
### `POST /api/agnet/user/tasks/{task_id}/deployment-draft`
### `POST /api/agent/user/tasks/{task_id}/deployment-draft`
把 HeicodeTask 快照转换成 Manager 可创建的 Agnet orchestration plan。
把 HeicodeTask 快照转换成 Manager 可创建的 Agent orchestration plan。
生产已验证:客户端只要能提供 task snapshot,就可以不依赖 Manager 再去拉 task,直接调用本接口生成 draft。
@@ -685,7 +685,7 @@ Authorization: Bearer <heicode_access_token>
| `sub_mode` | string | 是 | `agile` 或 `waterfall`,桌面客户端默认传 `agile` |
| `binding_scope` | string | 否 | 建议 `task-<task_id>` |
| `role_templates` | string[] | 否 | 默认 `["backend"]` |
| `default_model_id` | string | 否 | 子 Agnet 默认运行模型;生产默认建议 `claude-sonnet-4-6` |
| `default_model_id` | string | 否 | 子 Agent 默认运行模型;生产默认建议 `claude-sonnet-4-6` |
| `budget` | object | 否 | 不传使用默认预算 |
| `resource_grants` | array | 否 | 不传时 Manager 生成只读 task context grant |
@@ -717,7 +717,7 @@ Authorization: Bearer <heicode_access_token>
"newapi_group": "default"
},
"agent_runtime": {
"platform": "agnet",
"platform": "agent",
"agents": [
{
"role": "backend",
@@ -747,7 +747,7 @@ Authorization: Bearer <heicode_access_token>
## 7. 创建 Manager Deployment
### `POST /api/agnet/user/deployments`
### `POST /api/agent/user/deployments`
使用上一步 `orchestration_plan` 创建 Manager deployment。
@@ -783,7 +783,7 @@ Authorization: Bearer <heicode_access_token>
"newapi_group": "default"
},
"agent_runtime": {
"platform": "agnet",
"platform": "agent",
"agents": [
{
"role": "backend",
@@ -910,7 +910,7 @@ Authorization: Bearer <heicode_access_token>
### 8.1 列表
#### `GET /api/agnet/user/deployments`
#### `GET /api/agent/user/deployments`
响应:
@@ -938,13 +938,13 @@ Authorization: Bearer <heicode_access_token>
### 8.2 详情
#### `GET /api/agnet/user/deployments/{deployment_id}`
#### `GET /api/agent/user/deployments/{deployment_id}`
响应字段同列表单项,包含完整 `orchestration_plan`。
### 8.3 停止
#### `POST /api/agnet/user/deployments/{deployment_id}/stop`
#### `POST /api/agent/user/deployments/{deployment_id}/stop`
请求:
@@ -971,7 +971,7 @@ Authorization: Bearer <heicode_access_token>
### 9.1 Events
#### `GET /api/agnet/user/deployments/{deployment_id}/events`
#### `GET /api/agent/user/deployments/{deployment_id}/events`
用于展示 deployment 事件流。
@@ -996,7 +996,7 @@ Authorization: Bearer <heicode_access_token>
### 9.2 Logs
#### `GET /api/agnet/user/deployments/{deployment_id}/logs`
#### `GET /api/agent/user/deployments/{deployment_id}/logs`
用于展示最近日志。当前 Manager 未接真实 Runtime 时主要是审计日志。
@@ -1020,7 +1020,7 @@ Authorization: Bearer <heicode_access_token>
### 9.3 Metrics
#### `GET /api/agnet/user/deployments/{deployment_id}/metrics`
#### `GET /api/agent/user/deployments/{deployment_id}/metrics`
用于展示成本、耗时、token、资源指标。真实 Runtime 未接入时可能为空或为本地占位。
@@ -1039,7 +1039,7 @@ Authorization: Bearer <heicode_access_token>
### 9.4 Artifacts
#### `GET /api/agnet/user/deployments/{deployment_id}/artifacts`
#### `GET /api/agent/user/deployments/{deployment_id}/artifacts`
查询中间交付物和最终交付物摘要。注意:列表接口只返回摘要和引用,不能证明代码内容有效;客户端需要按需调用 content 接口并按“Artifact 展示和有效性判断”继续分类。
@@ -1070,7 +1070,7 @@ Authorization: Bearer <heicode_access_token>
}
```
#### `GET /api/agnet/user/deployments/{deployment_id}/artifacts/{artifact_id}/content`
#### `GET /api/agent/user/deployments/{deployment_id}/artifacts/{artifact_id}/content`
读取完整产物正文。客户端必须通过该接口判断产物是否真的是代码,而不是只看 `artifact_type`。
@@ -1113,7 +1113,7 @@ summary-only 示例片段:
### 9.5 SK Snapshots
#### `GET /api/agnet/user/deployments/{deployment_id}/sk-snapshots`
#### `GET /api/agent/user/deployments/{deployment_id}/sk-snapshots`
查询本轮任务使用的 SK 快照。
@@ -1147,7 +1147,7 @@ summary-only 示例片段:
### 9.6 Timeline
#### `GET /api/agnet/user/deployments/{deployment_id}/timeline`
#### `GET /api/agent/user/deployments/{deployment_id}/timeline`
聚合审计事件、Runtime callback、artifact、SK snapshot。桌面客户端推荐优先使用这个接口渲染“当前子环节进度”。
@@ -1206,7 +1206,7 @@ summary-only 示例片段:
### 10.1 查询待审批
#### `GET /api/agnet/approvals?status=pending&deployment_id={deployment_id}`
#### `GET /api/agent/approvals?status=pending&deployment_id={deployment_id}`
查询参数:
@@ -1236,7 +1236,7 @@ summary-only 示例片段:
"requires_credential": true,
"credential_lease_id": "",
"status": "pending",
"requested_by": "agnet-runtime",
"requested_by": "agent-runtime",
"request_reason": "需要写入功能分支",
"ttl_seconds": 900,
"expires_at": 1779850900000,
@@ -1249,7 +1249,7 @@ summary-only 示例片段:
### 10.2 同意审批
#### `POST /api/agnet/approvals/{approval_id}/approve`
#### `POST /api/agent/approvals/{approval_id}/approve`
请求:
@@ -1270,7 +1270,7 @@ summary-only 示例片段:
"credential_lease_id": "lease_xxx",
"credential_lease": {
"lease_id": "lease_xxx",
"credential_ref": "lease://agnet/lease_xxx",
"credential_ref": "lease://agent/lease_xxx",
"status": "active",
"expires_at": 1779850900000
}
@@ -1282,7 +1282,7 @@ summary-only 示例片段:
### 10.3 拒绝审批
#### `POST /api/agnet/approvals/{approval_id}/reject`
#### `POST /api/agent/approvals/{approval_id}/reject`
请求:
@@ -1307,7 +1307,7 @@ summary-only 示例片段:
### 10.4 查询 lease
#### `GET /api/agnet/credential-leases?status=active&deployment_id={deployment_id}`
#### `GET /api/agent/credential-leases?status=active&deployment_id={deployment_id}`
响应:
@@ -1318,7 +1318,7 @@ summary-only 示例片段:
"items": [
{
"lease_id": "lease_xxx",
"credential_ref": "lease://agnet/lease_xxx",
"credential_ref": "lease://agent/lease_xxx",
"approval_id": "appr_xxx",
"deployment_id": "dep_464a08b7b751",
"resource_id": "res_git_main",
@@ -1334,7 +1334,7 @@ summary-only 示例片段:
### 10.5 撤销 lease
#### `POST /api/agnet/credential-leases/{lease_id}/revoke`
#### `POST /api/agent/credential-leases/{lease_id}/revoke`
请求:
@@ -1358,9 +1358,9 @@ summary-only 示例片段:
## 11. 角色模板
### `GET /api/agnet/role-templates`
### `GET /api/agent/role-templates`
获取推荐子 Agnet 角色,桌面端可用于角色选择页。
获取推荐子 Agent 角色,桌面端可用于角色选择页。
响应:
@@ -1371,7 +1371,7 @@ summary-only 示例片段:
"items": [
{
"key": "backend",
"display_name": "Backend Agnet",
"display_name": "Backend Agent",
"summary": "实现后端接口、数据模型和服务逻辑",
"default_model": "gpt-5.4",
"default_permissions": ["repo:read", "repo:write"],
@@ -1382,7 +1382,7 @@ summary-only 示例片段:
}
```
> `default_model` 由 Manager 单一来源 `defaultAgnetModelID()` 提供(环境变量 `AGNET_DEFAULT_MODEL_ID`,默认生产已验证的 `gpt-5.4`)。Manager 不再回退 `agnet-model-<role>` 占位名;客户端如不指定 `default_model_id`,draft/runtime agent 会自动采用该单一默认值。
> `default_model` 由 Manager 单一来源 `defaultAgentModelID()` 提供(环境变量 `AGENT_DEFAULT_MODEL_ID`,默认生产已验证的 `gpt-5.4`)。Manager 不再回退 `agent-model-<role>` 占位名;客户端如不指定 `default_model_id`,draft/runtime agent 会自动采用该单一默认值。
## 12. 状态枚举
@@ -1393,12 +1393,12 @@ summary-only 示例片段:
| `draft` | 草稿 | 可继续编辑 |
| `configuring` | 等待回答追问 | 展示 followups |
| `running` | 可推进/运行中 | 创建 deployment 或展示 timeline |
| `awaiting_approval` | 等待审批 | 拉取 `/api/agnet/approvals` |
| `awaiting_approval` | 等待审批 | 拉取 `/api/agent/approvals` |
| `completed` | 完成 | 展示交付物 |
| `failed` | 失败 | 展示失败原因 |
| `paused` | 暂停 | 允许继续或停止 |
### Agnet Deployment.status
### Agent Deployment.status
| 状态 | 说明 |
|---|---|
@@ -1410,7 +1410,7 @@ summary-only 示例片段:
> 注意:`status` 由 Manager 接收 Runtime `deployment.status_changed` 后镜像写入。除上述规范值外,Runtime 若回传其他自定义状态字符串,Manager 会透传保存,客户端应对未知值做兜底(按非终态处理或显示原值)。
### Agnet Deployment.runtime_state
### Agent Deployment.runtime_state
| 状态 | 说明 |
|---|---|
@@ -1508,7 +1508,7 @@ while (current.status === 'configuring') {
const draft = await encryptedManagerRequest<Envelope<DeploymentDraft>>(
'POST',
`/api/agnet/user/tasks/${current.id}/deployment-draft`,
`/api/agent/user/tasks/${current.id}/deployment-draft`,
{
task: current,
sub_mode: 'agile',
@@ -1520,7 +1520,7 @@ const draft = await encryptedManagerRequest<Envelope<DeploymentDraft>>(
const deployment = await encryptedManagerRequest<Envelope<Deployment>>(
'POST',
'/api/agnet/user/deployments',
'/api/agent/user/deployments',
{ orchestration_plan: draft.data.orchestration_plan }
)
@@ -1528,13 +1528,13 @@ const deploymentId = deployment.data.deployment_id
setInterval(async () => {
const timeline = await manager.get<Envelope<DeploymentTimeline>>(
`/api/agnet/user/deployments/${deploymentId}/timeline`,
`/api/agent/user/deployments/${deploymentId}/timeline`,
{ headers: { 'New-Api-User': String(userId) } }
)
renderTimeline(timeline.data.timeline)
const approvals = await manager.get<Envelope<ApprovalList>>(
`/api/agnet/approvals?status=pending&deployment_id=${deploymentId}`,
`/api/agent/approvals?status=pending&deployment_id=${deploymentId}`,
{ headers: { 'New-Api-User': String(userId) } }
)
renderApprovals(approvals.data.items)
@@ -1542,13 +1542,13 @@ setInterval(async () => {
async function refreshSubDelivery(deploymentId: string) {
const diagnostics = await manager.get<Envelope<RuntimeDiagnostics>>(
`/api/agnet/user/deployments/${deploymentId}/runtime-diagnostics`,
`/api/agent/user/deployments/${deploymentId}/runtime-diagnostics`,
{ headers: { 'New-Api-User': String(userId) } }
)
renderRuntimeDiagnostics(diagnostics.data)
const artifacts = await manager.get<Envelope<ArtifactList>>(
`/api/agnet/user/deployments/${deploymentId}/artifacts`,
`/api/agent/user/deployments/${deploymentId}/artifacts`,
{ headers: { 'New-Api-User': String(userId) } }
)
renderArtifacts(artifacts.data.artifacts)
@@ -1558,7 +1558,7 @@ async function refreshSubDelivery(deploymentId: string) {
)
if (firstDeliverable) {
const content = await manager.get<string>(
`/api/agnet/user/deployments/${deploymentId}/artifacts/${firstDeliverable.artifact_id}/content`,
`/api/agent/user/deployments/${deploymentId}/artifacts/${firstDeliverable.artifact_id}/content`,
{
headers: { 'New-Api-User': String(userId) },
responseType: 'text',
@@ -1571,10 +1571,10 @@ async function refreshSubDelivery(deploymentId: string) {
兼容说明:
- 桌面客户端对 `POST` 等有 body 的 sub 请求走 V2 加密时,`/api/agnet/user/*` 不需要 `New-Api-User`,也不依赖浏览器 cookie。
- 桌面客户端对 `POST` 等有 body 的 sub 请求走 V2 加密时,`/api/agent/user/*` 不需要 `New-Api-User`,也不依赖浏览器 cookie。
- `GET` 查询接口本身没有请求 body,当前生产兼容路径仍使用 Manager session cookie + `New-Api-User`。如果桌面本地服务后续要完全脱离 session cookie 查询 timeline / artifact / approval,需要再补“无 body 的 V2 设备签名 GET”协议。
- `/api/heicode-auth/*` 仍必须带 `Authorization: Bearer <heicode_access_token>`,该 token 只用于上游 HeicodeTask 认证。
- 如果客户端临时还没有接入 V2 加密,只能作为调试兼容路径使用 Manager session + `New-Api-User` 调 `/api/agnet/user/*`;正式桌面流程不要依赖该路径。
- 如果客户端临时还没有接入 V2 加密,只能作为调试兼容路径使用 Manager session + `New-Api-User` 调 `/api/agent/user/*`;正式桌面流程不要依赖该路径。
## 15. 当前生产注意事项
@@ -8,24 +8,24 @@
当 Runtime 提供正式 `POST /api/swarms` 时,Manager 建议配置:
```bash
AGNET_RUNTIME_ENABLED=true
AGNET_RUNTIME_ASYNC=false
AGNET_RUNTIME_BASE_URL=http://<runtime-host>
AGNET_RUNTIME_SERVICE_TOKEN=<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
AGENT_RUNTIME_ENABLED=true
AGENT_RUNTIME_ASYNC=false
AGENT_RUNTIME_BASE_URL=http://<runtime-host>
AGENT_RUNTIME_SERVICE_TOKEN=<service-token>
AGENT_RUNTIME_HEALTH_PATH=/api/agent/health
AGENT_RUNTIME_CREATE_PATH=/api/swarms
AGENT_RUNTIME_STOP_PATH=/api/swarms/{swarm_id}/stop
AGENT_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id}
AGENT_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=<callback-token>
AGENT_RUNTIME_CALLBACK_URL=https://code.xinghanlab.com/api/agent/callbacks/swarm-events
AGENT_CALLBACK_TOKEN=<callback-token>
```
说明:
1. `AGNET_RUNTIME_SERVICE_TOKEN` 是 Manager 调 Runtime 的 Bearer token。
2. `AGNET_CALLBACK_TOKEN` 是 Runtime 回调 Manager 时放在 `X-Agnet-Service-Token` 的 token。
1. `AGENT_RUNTIME_SERVICE_TOKEN` 是 Manager 调 Runtime 的 Bearer token。
2. `AGENT_CALLBACK_TOKEN` 是 Runtime 回调 Manager 时放在 `X-Agent-Service-Token` 的 token。
3. 两个 token 可以不同,不要写进前端或日志。
4. 如果 Runtime 返回 `swarm_id`,Manager 会保存到 `runtime_swarm_id`。
5. 如果 Runtime 返回 `deployment_id`,Manager 会保存到 `runtime_deployment_id`。
@@ -35,18 +35,18 @@ AGNET_CALLBACK_TOKEN=<callback-token>
如果短期只用现有 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=<optional-if-supported>
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
AGENT_RUNTIME_ENABLED=true
AGENT_RUNTIME_ASYNC=false
AGENT_RUNTIME_BASE_URL=http://52.139.240.116:8000
AGENT_RUNTIME_SERVICE_TOKEN=<optional-if-supported>
AGENT_RUNTIME_HEALTH_PATH=/health
AGENT_RUNTIME_CREATE_PATH=/tasks
AGENT_RUNTIME_STOP_PATH=/tasks/{runtime_deployment_id}/stop
AGENT_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id}
AGENT_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=<callback-token>
AGENT_RUNTIME_CALLBACK_URL=https://code.xinghanlab.com/api/agent/callbacks/swarm-events
AGENT_CALLBACK_TOKEN=<callback-token>
```
注意:
@@ -60,9 +60,9 @@ AGNET_CALLBACK_TOKEN=<callback-token>
推荐使用 service token:
```http
POST /api/agnet/callbacks/swarm-events
X-Agnet-Service-Token: <callback-token>
X-Agnet-Event-Id: <event-id>
POST /api/agent/callbacks/swarm-events
X-Agent-Service-Token: <callback-token>
X-Agent-Event-Id: <event-id>
X-Correlation-ID: <correlation-id>
Content-Type: application/json
```
@@ -70,9 +70,9 @@ Content-Type: application/json
也支持 HMAC:
```http
X-Agnet-Event-Id: <event-id>
X-Agnet-Timestamp: <unix-ms>
X-Agnet-Signature: sha256=<hex>
X-Agent-Event-Id: <event-id>
X-Agent-Timestamp: <unix-ms>
X-Agent-Signature: sha256=<hex>
X-Correlation-ID: <correlation-id>
Content-Type: application/json
```
@@ -88,13 +88,13 @@ timestamp + "." + event_id + "." + raw_body
健康检查:
```bash
curl -sS "$AGNET_RUNTIME_BASE_URL$AGNET_RUNTIME_HEALTH_PATH"
curl -sS "$AGENT_RUNTIME_BASE_URL$AGENT_RUNTIME_HEALTH_PATH"
```
Manager 本地默认模拟链路:
```bash
curl -sS -X POST "https://code.xinghanlab.com/api/agnet/user/deployments/<manager-deployment-id>/simulate-events" \
curl -sS -X POST "https://code.xinghanlab.com/api/agent/user/deployments/<manager-deployment-id>/simulate-events" \
-H "Authorization: Bearer <manager-user-token>" \
-H "Content-Type: application/json" \
-d '{}'
@@ -105,10 +105,10 @@ curl -sS -X POST "https://code.xinghanlab.com/api/agnet/user/deployments/<manage
模拟 Runtime 回调 Manager:
```bash
curl -sS -X POST "https://code.xinghanlab.com/api/agnet/callbacks/swarm-events" \
curl -sS -X POST "https://code.xinghanlab.com/api/agent/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-Agent-Service-Token: $AGENT_CALLBACK_TOKEN" \
-H "X-Agent-Event-Id: evt-smoke-001" \
-H "X-Correlation-ID: corr-smoke-001" \
-d '{
"event_id": "evt-smoke-001",
@@ -21,9 +21,9 @@
## 2. §5 审计日志的敏感字段 redact
**当前现象**:mcp-server `/api/agnet/audit` 返回的条目里 `payload` 字段可能包含 token / secret 原文(按 §5 设计应在服务端 mask)。
**当前现象**:mcp-server `/api/agent/audit` 返回的条目里 `payload` 字段可能包含 token / secret 原文(按 §5 设计应在服务端 mask)。
**前端兜底**:`src/features/agnet-console/pages.tsx::maskIfSecret()` 做了第二层 mask(按 key 名匹配 `secret|token|password|api_key`)。
**前端兜底**:`src/features/agent-console/pages.tsx::maskIfSecret()` 做了第二层 mask(按 key 名匹配 `secret|token|password|api_key`)。
**建议**:mcp-server 在写审计或读出时统一 mask,前端可以删掉那一层。
@@ -41,13 +41,13 @@
**建议**:维持当前字段名即可。如果未来引入按模型聚合,建议另起 endpoint,不要在 `items` 里混。
## 5. §5 `/api/agnet/*` stub 数据
## 5. §5 `/api/agent/*` stub 数据
**当前**:mcp-server 内存 dict(按 §5.0 设计),无 agent-manager 真实转发。
**阻塞解除条件**:
- agent-manager 侧给 mcp-server 派发 service token
- mcp-server 把 stub 换成对 `http://20.212.121.126` 的真实转发;域名 `agent-manager.taijiagnet.com` 的 DNS / HTTPS 后续单独修复,不作为当前联调阻塞项
- mcp-server 把 stub 换成对 `http://20.212.121.126` 的真实转发;域名 `agent-manager.taijiagent.com` 的 DNS / HTTPS 后续单独修复,不作为当前联调阻塞项
Manager 前端已按 §5 契约消费,stub → 真实数据无需前端改动。
@@ -27,14 +27,14 @@ Agent Manager 需要做的是:接收 Manager 生成的 deployment payload,
| 能力 | 状态 | 说明 |
|---|---|---|
| HeicodeTask -> deployment draft | 已完成 | `POST /api/agnet/user/tasks/{task_id}/deployment-draft` |
| 用户态 deployment 创建 | 已完成 | `POST /api/agnet/user/deployments` |
| HeicodeTask -> deployment draft | 已完成 | `POST /api/agent/user/tasks/{task_id}/deployment-draft` |
| 用户态 deployment 创建 | 已完成 | `POST /api/agent/user/deployments` |
| Runtime create bridge | 已完成 | 通过环境变量调用 Agent Manager |
| Runtime stop bridge | 已完成 | 停止时同步 Runtime |
| callback 接收 | 已完成 | `POST /api/agnet/callbacks/swarm-events` |
| callback 接收 | 已完成 | `POST /api/agent/callbacks/swarm-events` |
| phase / timeline 聚合 | 已完成 | deployment timeline 聚合 audit、callback、artifact、SK |
| artifact 查询 | 已完成 | `GET /api/agnet/user/deployments/{deployment_id}/artifacts` |
| approval 查询/approve/reject | 已完成 | `/api/agnet/approvals` |
| artifact 查询 | 已完成 | `GET /api/agent/user/deployments/{deployment_id}/artifacts` |
| approval 查询/approve/reject | 已完成 | `/api/agent/approvals` |
| V2 body 加密 | 已完成 | 桌面客户端到 Manager 的 POST 请求复用模型调用加密协议 |
| 生产验证 | 已完成核心链路 | 生产已验证 Manager `1.4.18` 可通过 `/api/swarms` 创建普通 sub run,持久化 `runtime_swarm_id=swm_*`,Agent Manager 自动 callback 可写入 `events/timeline`;`1.4.19` 补齐 callback 后反写 deployment 快照 |
| callback 状态反写 | 已完成 | Manager 接收 `deployment.status_changed`、`phase.changed`、`timeline.updated`、`agent.started/completed/crashed` 后,会同步更新 deployment `status/phase/runtime_state/agent_instances`,避免详情页长期停留 `initializing/pending` |
@@ -44,7 +44,7 @@ Agent Manager 需要做的是:接收 Manager 生成的 deployment payload,
| 任务 | 必需 | 原因 | 验收 |
|---|---:|---|---|
| 接收 Manager 创建请求 | 是 | Manager 会把普通 sub deployment 发送给 Agent Manager | `POST /api/agnet/deployments` 或配置的 create path 返回 2xx |
| 接收 Manager 创建请求 | 是 | Manager 会把普通 sub deployment 发送给 Agent Manager | `POST /api/agent/deployments` 或配置的 create path 返回 2xx |
| 支持 `sub_mode=agile` | 是 | 普通敏捷模式核心标识 | 不认识时不能按蜂群 task graph 强制处理 |
| 支持 `agile_context` | 是 | 用于阶段、检查点、验收标准和下一步动作 | Runtime 能读取并在回调里更新 stage/checkpoint |
| 支持阶段状态回传 | 是 | 客户端需要知道当前处在需求/设计/开发/测试/修复/部署哪个环节 | 回调 `phase.changed` 或 `timeline.updated` |
@@ -60,13 +60,13 @@ Agent Manager 需要做的是:接收 Manager 生成的 deployment payload,
Manager 当前默认 create path:
```text
AGNET_RUNTIME_CREATE_PATH=/api/agnet/deployments
AGENT_RUNTIME_CREATE_PATH=/api/agent/deployments
```
如果 Agent Manager 统一使用 `/api/swarms`,Manager 也可以配置切过去。但普通 sub 敏捷模式建议先支持:
```http
POST /api/agnet/deployments
POST /api/agent/deployments
Authorization: Bearer <service_token>
X-User-ID: <manager_user_id>
X-Binding-Scope: <binding_scope>
@@ -114,7 +114,7 @@ Content-Type: application/json
"requires_user_approval": false
},
"agent_runtime": {
"platform": "agnet",
"platform": "agent",
"agents": [
{
"role": "backend",
@@ -163,8 +163,8 @@ Content-Type: application/json
}
],
"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",
"url": "https://code.xinghanlab.com/api/agent/callbacks/swarm-events",
"signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agent-callback-signing-key",
"subscribed_events": [
"deployment.status_changed",
"phase.changed",
@@ -260,7 +260,7 @@ Agent Manager 至少要支持以下阶段语义,并通过 callback 回写当
联调前可以先请求 Manager 当前接受的 callback schema:
```http
GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema
GET https://code.xinghanlab.com/api/agent/callbacks/swarm-events/schema
```
该接口只返回事件类型、分类和必填字段,不返回任何 token 或密钥。普通 sub 敏捷重点核对 `phase.changed`、`timeline.updated`、`artifact.created`、`approval.requested`、`sk_tool.*` 和 `budget.alert`。
@@ -364,7 +364,7 @@ POST /api/swarms/{swarm_id}/approvals/{approval_id}
如果普通 sub 不使用 `swarm_id`,可让 Manager 配置为:
```text
AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/agnet/deployments/{deployment_id}/approvals/{approval_id}
AGENT_RUNTIME_APPROVAL_DECISION_PATH=/api/agent/deployments/{deployment_id}/approvals/{approval_id}
```
Runtime 需要接收:
@@ -380,7 +380,7 @@ Runtime 需要接收:
"resource_type": "git",
"target_role": "backend",
"requires_credential": true,
"credential_ref": "lease://agnet/lease_xxx",
"credential_ref": "lease://agent/lease_xxx",
"lease_id": "lease_xxx",
"lease_expires_at": 1779850900000
}
@@ -489,7 +489,7 @@ Agent Manager 回传 usage 时建议至少包含:
| 9 | Runtime 回调 completed/failed | Manager detail/timeline 显示终态 |
| 10 | Runtime 回调用量 | Manager 能看到 model token/cost 或 usage 摘要,且与 deployment/task/role/correlation 关联 |
| 11 | 查日志 | 不出现明文密钥,不丢 `correlation_id` |
| 12 | 生产路由复核 | `GET /api/agnet/callbacks/swarm-events/schema` 返回 200,用户态/后台态接口使用有效登录态通过 smoke |
| 12 | 生产路由复核 | `GET /api/agent/callbacks/swarm-events/schema` 返回 200,用户态/后台态接口使用有效登录态通过 smoke |
## 13. Agent Manager 不需要处理的内容
@@ -509,8 +509,8 @@ Agent Manager 回传 usage 时建议至少包含:
| 真实 Runtime 是否完整消费 `agile_context` | Agent Manager | 需要确认字段被使用,不只是透传 |
| 阶段状态持续回传 | Agent Manager | 2026-05-31 生产复核已收到 running/completed、phase、timeline、task.completed 等真实回调;后续需在更复杂多阶段任务中继续验证 |
| artifact 真实产出 | Agent Manager | 2026-05-31 生产复核已收到业务 `code_patch` artifact,并可通过 Manager content 代理下载;Git/部署 URL 类产物仍按具体任务继续验证 |
| approval decision 接收路径 | Agent Manager + Manager 配置 | 需确认最终路径是 `/api/swarms/...` 还是 `/api/agnet/deployments/...` |
| approval decision 接收路径 | Agent Manager + Manager 配置 | 需确认最终路径是 `/api/swarms/...` 还是 `/api/agent/deployments/...` |
| 用量回传 | Agent Manager + NewAPI/CodeGW | 2026-05-31 生产复核已回传 `tokens_used=2682`、`newapi_request_id=chatcmpl-DlbZce3VZnsv5DptBILFoiRYy5ZHN`;后续需继续按 user/deployment/task/role 归属 |
| Agent 运行费用真实结算 | Agent Manager + Manager | Runtime 已能回传 token usage;真实费用结算仍需按 Manager/NewAPI 订阅/钱包规则和 Runtime usage 口径统一 |
| 生产 callback schema 路由 | Manager 部署/路由 | 本地代码和测试已覆盖,2026-05-28 生产公开访问 `/api/agnet/callbacks/swarm-events/schema` 返回 404,需要重新上线或核对生产镜像/路由 |
| 生产 callback schema 路由 | Manager 部署/路由 | 本地代码和测试已覆盖,2026-05-28 生产公开访问 `/api/agent/callbacks/swarm-events/schema` 返回 404,需要重新上线或核对生产镜像/路由 |
| 无 body GET 的 V2 签名 | 客户端 + Manager | 这是客户端全链路无 cookie 的后续项,不阻塞 Agent Manager 创建/回调联调 |
@@ -14,10 +14,10 @@ Agent Manager / Swarm Runtime 当前还需要补齐或确认的是:真实创
| 能力 | 状态 | 说明 |
|---|---|---|
| Runtime 创建桥接 | 已完成 | Manager 可通过 `AGNET_RUNTIME_BASE_URL` + `AGNET_RUNTIME_CREATE_PATH` 调 Agent Manager |
| Runtime 停止桥接 | 已完成 | 默认 `POST /api/agnet/deployments/{deployment_id}/stop` |
| Runtime 创建桥接 | 已完成 | Manager 可通过 `AGENT_RUNTIME_BASE_URL` + `AGENT_RUNTIME_CREATE_PATH` 调 Agent Manager |
| Runtime 停止桥接 | 已完成 | 默认 `POST /api/agent/deployments/{deployment_id}/stop` |
| `/api/swarms` adapter | 已完成 | Manager 用户态入口已存在,后续可切真实 Runtime |
| callback 接收 | 已完成 | `POST /api/agnet/callbacks/swarm-events` |
| callback 接收 | 已完成 | `POST /api/agent/callbacks/swarm-events` |
| callback 鉴权 | 已完成 | 支持 service token 或 HMAC |
| callback 幂等 | 已完成 | 按 `event_id` / `idempotency_key` 去重 |
| task flow 校验 | 已完成 | `task.*` / `handoff.*` 缺关键字段会返回 `CALLBACK_SCHEMA_INVALID` |
@@ -58,11 +58,11 @@ Content-Type: application/json
Manager 当前可通过环境变量切换:
```text
AGNET_RUNTIME_BASE_URL=http://20.212.121.126
AGNET_RUNTIME_CREATE_PATH=/api/swarms
AGNET_RUNTIME_HEALTH_PATH=/api/agnet/health
AGNET_RUNTIME_STOP_PATH=/api/agnet/deployments/{deployment_id}/stop
AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id}
AGENT_RUNTIME_BASE_URL=http://20.212.121.126
AGENT_RUNTIME_CREATE_PATH=/api/swarms
AGENT_RUNTIME_HEALTH_PATH=/api/agent/health
AGENT_RUNTIME_STOP_PATH=/api/agent/deployments/{deployment_id}/stop
AGENT_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_id}
```
请求体核心形状:
@@ -116,8 +116,8 @@ AGNET_RUNTIME_APPROVAL_DECISION_PATH=/api/swarms/{swarm_id}/approvals/{approval_
}
],
"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",
"url": "https://code.xinghanlab.com/api/agent/callbacks/swarm-events",
"signing_secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/agent-callback-signing-key",
"subscribed_events": [
"deployment.status_changed",
"task.created",
@@ -180,9 +180,9 @@ Manager 解析规则:
统一入口:
```http
POST https://code.xinghanlab.com/api/agnet/callbacks/swarm-events
X-Agnet-Service-Token: <callback_token>
X-Agnet-Event-Id: <event_id>
POST https://code.xinghanlab.com/api/agent/callbacks/swarm-events
X-Agent-Service-Token: <callback_token>
X-Agent-Event-Id: <event_id>
X-Correlation-ID: <correlation_id>
Content-Type: application/json
```
@@ -190,8 +190,8 @@ Content-Type: application/json
也支持 HMAC:
```http
X-Agnet-Timestamp: <unix_ms>
X-Agnet-Signature: sha256=<hex>
X-Agent-Timestamp: <unix_ms>
X-Agent-Signature: sha256=<hex>
```
签名内容:
@@ -203,7 +203,7 @@ timestamp + "." + event_id + "." + raw_body
Agent Manager / 蜂群侧可以先拉取 Manager 当前接受的事件 schema:
```http
GET https://code.xinghanlab.com/api/agnet/callbacks/swarm-events/schema
GET https://code.xinghanlab.com/api/agent/callbacks/swarm-events/schema
```
该接口不返回任何 token 或密钥,只返回事件类型、分类和必填字段,用于联调前核对 `task.*`、`handoff.*`、`artifact.created`、`approval.requested` 等 callback payload 是否符合 Manager 接收规则。
@@ -366,7 +366,7 @@ Content-Type: application/json
"risk_level": "high",
"requires_credential": true,
"user_id": "22",
"credential_ref": "lease://agnet/lease_xxx",
"credential_ref": "lease://agent/lease_xxx",
"lease_id": "lease_xxx",
"lease_expires_at": 1779850900000
}
@@ -379,7 +379,7 @@ Content-Type: application/json
默认:
```http
POST /api/agnet/deployments/{runtime_deployment_id}/stop
POST /api/agent/deployments/{runtime_deployment_id}/stop
Authorization: Bearer <service_token>
Content-Type: application/json
```
@@ -410,7 +410,7 @@ Content-Type: application/json
| 用例 | 操作 | 期望 |
|---|---|---|
| health | `GET /api/agnet/health` | 200,`status=healthy` |
| health | `GET /api/agent/health` | 200,`status=healthy` |
| create | Manager 创建 deployment | Runtime 返回 `deployment_id` 和 `swarm_id` |
| task flow | Runtime 回调 `task.created/claimed/heartbeat/blocked/retried/completed` | Manager timeline 和页面 Sub task flow 可见 |
| handoff | Runtime 回调 `handoff.requested/completed` | Manager 页面可见 from/to role |
@@ -433,7 +433,7 @@ Content-Type: application/json
## 12. 联调顺序建议
1. Agent Manager 确认 health、create、stop、approval decision 路径。
2. Manager 配置 `AGNET_RUNTIME_CREATE_PATH=/api/swarms` 后创建一个真实 deployment。
2. Manager 配置 `AGENT_RUNTIME_CREATE_PATH=/api/swarms` 后创建一个真实 deployment。
3. Agent Manager 回调 `task.created`、`task.claimed`、`task.heartbeat`。
4. 回调 `task.blocked`、`handoff.requested`、`handoff.completed`、`task.retried`。
5. 回调 `artifact.created`。
+22 -22
View File
@@ -4,7 +4,7 @@
## P0:边界收敛
状态:文档边界已收敛。当前 `docs/` 只保留 `heicode.md`、`plan.md` 和已上线登录接口文档作为实施依据;旧 Agnet API 草案、旧 M1-M5 计划和旧架构说明不再作为开发输入。现有代码中仍可能存在过渡期的 `sk_sources`、Agnet control plane 或部署计划命名,不能反向覆盖本文档边界。
状态:文档边界已收敛。当前 `docs/` 只保留 `heicode.md`、`plan.md` 和已上线登录接口文档作为实施依据;旧 Agent API 草案、旧 M1-M5 计划和旧架构说明不再作为开发输入。现有代码中仍可能存在过渡期的 `sk_sources`、Agent control plane 或部署计划命名,不能反向覆盖本文档边界。
目标:让团队只围绕一套产品和架构边界协作。
@@ -12,24 +12,24 @@
- 以 [`heicode.md`](./heicode.md) 作为当前产品与架构共识。
- 保留已上线登录接口文档。
- 不再维护旧 Agnet API 草案和旧 M1-M5 计划。
- 后续所有实现前先确认是否符合 Manager / Agnet / NewAPI / Secret Store 的边界。
- 不再维护旧 Agent API 草案和旧 M1-M5 计划。
- 后续所有实现前先确认是否符合 Manager / Agent / NewAPI / Secret Store 的边界。
- 需求和边界没有想清楚前,不改代码。
验收:
- `docs/` 中没有多套互相冲突的 Agnet、NewAPI 或 Manager 计划。
- `docs/` 中没有多套互相冲突的 Agent、NewAPI 或 Manager 计划。
- 新需求讨论先落到文档共识,再进入实现。
## P1:Manager 资源模型
目标:把“Git 来源”升级为面向子 Agnet 的统一资源绑定模型。
目标:把“Git 来源”升级为面向子 Agent 的统一资源绑定模型。
任务:
- 将当前 Git 来源抽象为资源绑定模型。
- 增加资源类型:Git、SK、项目文档、云账号、单项云资源。
- 增加 Resource Grant,用于把资源分配给登录用户、绑定 Git/SK/云资源范围、角色和子 Agnet。
- 增加 Resource Grant,用于把资源分配给登录用户、绑定 Git/SK/云资源范围、角色和子 Agent。
- 定义资源元数据、权限范围、约束、状态和审计字段。
- 前端从单点功能页逐步走向“绑定资源 -> 分配角色 -> 部署确认”的主流程。
@@ -39,13 +39,13 @@
1. 后端先落库资源绑定和 Resource Grant 两类记录,不在本阶段实现 Secret Broker 的真实写入。
2. Resource Binding 表达登录用户绑定的资源元数据:资源类型、名称、外部标识、可见元数据、权限范围、约束、状态、`secret_ref` 和审计字段。
3. Resource Grant 表达授权关系:user、resource、binding scope、role、子 Agnet 标识、允许动作、限制条件、状态、过期时间和审计字段。
3. Resource Grant 表达授权关系:user、resource、binding scope、role、子 Agent 标识、允许动作、限制条件、状态、过期时间和审计字段。
4. 提供只返回元数据和 `secret_ref` 的列表、详情、创建、授权、撤销接口;任何接口响应、日志和 Markdown 产物都不得包含真实密钥。
5. 生成一份 permission manifest 示例,用结构化数据证明“某登录用户把某个绑定资源授予某个子 Agnet 角色使用”。
5. 生成一份 permission manifest 示例,用结构化数据证明“某登录用户把某个绑定资源授予某个子 Agent 角色使用”。
验收:
- Manager 能表达“某登录用户把某个绑定资源授予某个子 Agnet 角色使用”。
- Manager 能表达“某登录用户把某个绑定资源授予某个子 Agent 角色使用”。
- 数据库不保存明文密钥,只保存 `secret_ref`。
- P1 测试样例能覆盖 Git、SK、项目文档、云账号和单项云资源五类资源的元数据建模。
- 撤销 Resource Grant 后,对应 permission manifest 不再包含该授权。
@@ -68,23 +68,23 @@
- Git token、云密钥、SSH key、数据库密码不会进入 Git、Markdown、前端响应或普通日志。
- 用户可以授权和撤销资源,平台负责实际凭证托管。
## P3:Agnet 平台 AKS 身份接入
## P3:Agent 平台 AKS 身份接入
目标:让子 Agnet 在 AKS 上按最小权限访问被授权资源。
目标:让子 Agent 在 AKS 上按最小权限访问被授权资源。
任务:
- Agnet 平台支持 deployment / role 到 Kubernetes ServiceAccount 的映射。
- Agent 平台支持 deployment / role 到 Kubernetes ServiceAccount 的映射。
- 支持 Vault Kubernetes Auth 或等价 Workload Identity。
- 支持按 user / resource binding / role 生成密钥访问策略。
- 子 Agnet 运行时只能访问被授权的 secret。
- 子 Agent 运行时只能访问被授权的 secret。
- 普通开发资源支持受控注入。
- 高危操作审批只在客户端完成;审批通过后允许向子 Agnet 注入密钥保管器派生的短期、最小权限凭证。
- 高危操作审批只在客户端完成;审批通过后允许向子 Agent 注入密钥保管器派生的短期、最小权限凭证。
验收:
- 子 Agnet 不保存长期密钥。
- 撤销 Resource Grant 后,子 Agnet 无法继续访问对应资源。
- 子 Agent 不保存长期密钥。
- 撤销 Resource Grant 后,子 Agent 无法继续访问对应资源。
- 高危资源访问有审计记录。
## P4:NewAPI 解耦
@@ -98,8 +98,8 @@
- Manager 通过服务凭据调用 NewAPI。
- Manager 展示普通用户需要的模型、余额、额度、调用日志。
- 隐藏渠道管理、价格配置、模型供应商后台配置和 NewAPI 管理员能力。
- 建立 Heicode/Agnet 登录用户 `user.id`、`channelId` 与 NewAPI user / token / group / quota / usage 的映射。
- 子 Agnet 的运行模型、模型 profile 和实例数归 Agnet 平台部署配置管理,不和 NewAPI 扣费映射混用。
- 建立 Heicode/Agent 登录用户 `user.id`、`channelId` 与 NewAPI user / token / group / quota / usage 的映射。
- 子 Agent 的运行模型、模型 profile 和实例数归 Agent 平台部署配置管理,不和 NewAPI 扣费映射混用。
验收:
@@ -109,18 +109,18 @@
## P5:部署和审计闭环
目标:跑通从用户想法到子 Agnet 部署、执行、观测和审计的闭环。
目标:跑通从用户想法到子 Agent 部署、执行、观测和审计的闭环。
任务:
- Manager 生成 AGENT.md、resource context 和 permission manifest。
- Agnet 平台部署子 Agnet 后回传 deployment、agent instance、状态和事件。
- Agent 平台部署子 Agent 后回传 deployment、agent instance、状态和事件。
- Manager 展示活动状态、失败原因、资源使用记录、模型调用记录和审计日志。
- 对高危权限增加审批、撤销和运行中失效机制。
- 为每次部署保留可追溯的资源、权限、模型和上下文快照。
验收:
- 用户能看到每个子 Agnet 的角色、模型、资源权限、运行状态和失败原因。
- 审计能回答谁在什么时候让哪个子 Agnet 使用了什么资源。
- 用户能看到每个子 Agent 的角色、模型、资源权限、运行状态和失败原因。
- 审计能回答谁在什么时候让哪个子 Agent 使用了什么资源。
- Markdown 只作为上下文,permission manifest 才是系统执行依据。
+14 -14
View File
@@ -8,7 +8,7 @@ Heicode
Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向的是“从想法到上线”的完整开发过程,而不是单个模型聊天窗口、CodeGW 管理后台或简单的 Agent 控制台。
用户只需要注册登录,输入想法和约束,Heicode 就帮助用户组织子 Agnet 团队,接入用户授权的 Git、SK、项目文档和云资源,完成产品设计、代码开发、代码检查、部署、观测和后续维护。
用户只需要注册登录,输入想法和约束,Heicode 就帮助用户组织子 Agent 团队,接入用户授权的 Git、SK、项目文档和云资源,完成产品设计、代码开发、代码检查、部署、观测和后续维护。
## 目标用户
@@ -23,11 +23,11 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向
1. 把自然语言想法变成可执行的软件开发任务。
2. 把 Git、文档、SK、云资源变成可授权、可撤销、可审计的资源。
3. 把子 Agnet 的角色、权限、模型和上下文组织成可运行的开发团队。
4. 把持续开发拆成需求、设计、开发、测试、修复、部署等子环节,由 Agnet 持续推进。
5. 让 Agnet 在执行过程中可以调用项目 SK 工具和外部能力,而不是只做一次性任务分发。
3. 把子 Agent 的角色、权限、模型和上下文组织成可运行的开发团队。
4. 把持续开发拆成需求、设计、开发、测试、修复、部署等子环节,由 Agent 持续推进。
5. 让 Agent 在执行过程中可以调用项目 SK 工具和外部能力,而不是只做一次性任务分发。
6. 把模型调用、余额、额度和日志统一展示给用户。
7. 把长期密钥放入密钥保管器,只给子 Agnet 短期、最小权限凭证。
7. 把长期密钥放入密钥保管器,只给子 Agent 短期、最小权限凭证。
8. 把开发、检查、部署、维护纳入同一个生命周期闭环。
## 产品形态
@@ -35,12 +35,12 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向
| 产品面 | 说明 |
|--------|------|
| Heicode 客户端 | 用户主体验,负责本地对话、输入想法、继续开发、查看执行反馈、接收交付结果和高危审批,只登录 Heicode,只使用 Heicode 提供的模型 |
| Heicode Manager / 浏览器控制台 | 辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看 |
| Agnet 平台 | 执行层,负责部署和运行子 Agnet,在任务推进过程中完成需求、开发、测试、修复、交付与部署,并回传日志、状态、事件和指标 |
| Heicode Manager / 浏览器控制台 | 辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agent 部署、任务状态总览、模型余额与用量、审计与日志查看 |
| Agent 平台 | 执行层,负责部署和运行子 Agent,在任务推进过程中完成需求、开发、测试、修复、交付与部署,并回传日志、状态、事件和指标 |
| CodeGW | 内部模型网关与计费服务,普通用户不直接进入后台 |
| 密钥保管器 | Azure Key Vault 实现,保存长期凭证引用,按审批和权限提供短期访问能力 |
说明:Heicode 客户端是用户主体验,用户不在网页上编码。Heicode Manager 是浏览器里的辅助控制台,承担资源、部署、状态、余额、审计和下载等辅助操作。真正持续推进任务的是 Heicode 调度下的 Agnet 执行闭环,Agnet 在过程中还可以调用已授权的 SK 工具和外部能力,最终完成交付与部署并把结果回传到客户端。
说明:Heicode 客户端是用户主体验,用户不在网页上编码。Heicode Manager 是浏览器里的辅助控制台,承担资源、部署、状态、余额、审计和下载等辅助操作。真正持续推进任务的是 Heicode 调度下的 Agent 执行闭环,Agent 在过程中还可以调用已授权的 SK 工具和外部能力,最终完成交付与部署并把结果回传到客户端。
## 核心流程
@@ -49,12 +49,12 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向
-> 输入产品想法
-> 生成产品文档和任务计划
-> 在 Manager 绑定 Git 和云资源
-> 在 Manager 部署 Agnet
-> 在 Manager 部署 Agent
-> 客户端继续追加需求和修正方向
-> Agnet 按子环节执行需求、开发、测试、修复
-> Agnet 按需要调用已授权 SK 工具
-> Agent 按子环节执行需求、开发、测试、修复
-> Agent 按需要调用已授权 SK 工具
-> 客户端确认高危操作
-> Agnet 完成交付物整理和部署
-> Agent 完成交付物整理和部署
-> Manager 查看状态、余额、审计和下载
-> 客户端展示执行反馈和交付结果
-> 持续维护和升级
@@ -72,11 +72,11 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向
## 产品承诺
1. 用户不需要理解底层模型供应商。
2. 用户不需要把密钥交给子 Agnet 长期保存。
2. 用户不需要把密钥交给子 Agent 长期保存。
3. 用户可以清楚知道哪些资源被哪个角色使用。
4. 高危操作必须经过客户端审批。
5. 所有关键动作都能审计。
## 当前一句话卖点
> 从一个想法开始,让 Heicode 组织 AI 开发团队,持续调用 Agnet 完成开发、测试、交付与部署,安全接入你的代码和云资源,把产品推进到生产环境。
> 从一个想法开始,让 Heicode 组织 AI 开发团队,持续调用 Agent 完成开发、测试、交付与部署,安全接入你的代码和云资源,把产品推进到生产环境。
@@ -40,7 +40,7 @@
首屏辅助信息:
- 绑定 Git 与云资源
- 生成子 Agnet 团队
- 生成子 Agent 团队
- 密钥保管器托管凭证
- CodeGW 统一模型与用量
@@ -91,7 +91,7 @@
| 想法转任务 | 把自然语言目标转成产品文档、任务和角色 |
| 资源绑定 | 接入 Git、SK、项目文档、云账号和云资源 |
| 权限控制 | 按角色分配 Resource Grant 和审批规则 |
| 执行闭环 | 子 Agnet 持续推进开发、测试、修复和部署,可调用 SK 工具,Heicode 展示日志和审计 |
| 执行闭环 | 子 Agent 持续推进开发、测试、修复和部署,可调用 SK 工具,Heicode 展示日志和审计 |
### 4. 工作流区
@@ -120,7 +120,7 @@
- 长期密钥进入密钥保管器。
- Heicode 服务端数据库只保存 `secret_ref`。
- 子 Agnet 只拿短期凭证。
- 子 Agent 只拿短期凭证。
- 高危操作在客户端审批。
- 日志、Markdown、前端响应不显示明文密钥。
@@ -134,14 +134,14 @@
|------|------|
| Heicode Manager | 资源、部署、状态、用量和审计辅助控制台 |
| 客户端 | 主对话体验、任务推进和高危审批 |
| Agnet 平台 | AKS 执行层 |
| Agent 平台 | AKS 执行层 |
| CodeGW | 模型网关与用量 |
| Azure Key Vault | 密钥保管器 |
补充说明:
- 用户主要在客户端对话和推进任务。
- Manager 主要承担资源准备、Agnet 部署、用量和审计辅助。
- Manager 主要承担资源准备、Agent 部署、用量和审计辅助。
### 7. 适用场景区
@@ -21,13 +21,13 @@
| 1. 登录 | 注册或登录 Heicode | 获取用户身份和 channelId | 用户会话 |
| 2. 输入想法 | 描述要做的产品或任务 | 生成需求摘要和任务草案 | 需求草案 |
| 3. 绑定资源 | 授权 Git、文档、SK、云资源 | 保存资源元数据和 `secret_ref` | Resource Binding |
| 4. 生成团队 | 确认开发方法和角色 | 推荐子 Agnet 角色 | 角色方案 |
| 4. 生成团队 | 确认开发方法和角色 | 推荐子 Agent 角色 | 角色方案 |
| 5. 分配权限 | 确认每个角色能用什么 | 生成 Resource Grant | 权限清单 |
| 6. 审批高危操作 | 在客户端确认高危动作 | 记录 approval | 审批记录 |
| 7. 执行任务 | 持续追加需求、查看进度 | Agnet 平台运行子 Agnet,按子环节推进开发 | 状态、日志、事件 |
| 8. SK 工具调用 | 允许平台使用技能能力 | Agnet 按权限调用 SK 工具和外部能力 | 中间产物、检查结果 |
| 9. 交付上线 | 确认部署结果 | Agnet 完成交付整理、部署并回写审计 | 生产服务 |
| 10. 维护升级 | 提出迭代或修复 | 复用上下文和权限再次进入 Agnet 闭环 | 新版本计划 |
| 7. 执行任务 | 持续追加需求、查看进度 | Agent 平台运行子 Agent,按子环节推进开发 | 状态、日志、事件 |
| 8. SK 工具调用 | 允许平台使用技能能力 | Agent 按权限调用 SK 工具和外部能力 | 中间产物、检查结果 |
| 9. 交付上线 | 确认部署结果 | Agent 完成交付整理、部署并回写审计 | 生产服务 |
| 10. 维护升级 | 提出迭代或修复 | 复用上下文和权限再次进入 Agent 闭环 | 新版本计划 |
## 关键任务状态流程
@@ -63,7 +63,7 @@
- 产品文档草案。
- 原型描述。
- 推荐资源需求。
- 推荐子 Agnet 角色。
- 推荐子 Agent 角色。
### 3. 任务上下文准备
@@ -84,7 +84,7 @@ Heicode 判断本任务缺少哪些上下文
默认展示角色视角:
```text
Backend Agnet
Backend Agent
- 可读写:后端代码路径
- 可读:项目文档
- 可部署:测试环境
@@ -98,20 +98,20 @@ Backend Agnet
展示:
- 本次目标。
- 子 Agnet 数量和角色。
- 子 Agent 数量和角色。
- 每个角色使用的资源。
- 是否会访问密钥。
- 是否会部署云资源。
- 预计模型预算。
- 审批项。
用户确认后,Heicode 生成 Agnet 平台 payload。
用户确认后,Heicode 生成 Agent 平台 payload。
### 6. 执行中的任务空间
展示:
- 子 Agnet 活跃状态。
- 子 Agent 活跃状态。
- 当前步骤。
- 最近日志。
- 失败原因。
@@ -127,21 +127,21 @@ Backend Agnet
- 撤销资源授权。
- 发起修复或继续迭代。
### 7. Agnet 执行闭环
### 7. Agent 执行闭环
Heicode 不是只把任务丢给 Agnet 一次就结束,而是会在开发过程中持续调用 Agnet 完成子环节。
Heicode 不是只把任务丢给 Agent 一次就结束,而是会在开发过程中持续调用 Agent 完成子环节。
闭环应表达为:
```text
客户端输入目标或追加需求
-> Heicode 生成下一步任务
-> Agnet 执行需求/设计/开发/测试/修复中的当前子环节
-> Agnet 按需要调用已授权的 SK 工具
-> Agent 执行需求/设计/开发/测试/修复中的当前子环节
-> Agent 按需要调用已授权的 SK 工具
-> Heicode 回传中间结果给客户端
-> 用户继续追问、修正或审批
-> Agnet 继续下一子环节
-> 最终由 Agnet 完成交付整理与部署
-> Agent 继续下一子环节
-> 最终由 Agent 完成交付整理与部署
```
这意味着用户看到的不是一次性“已部署 Agent”,而是一个可连续推进的开发循环。
@@ -177,8 +177,8 @@ MVP 最小路径:
6. 选择推荐角色。
7. 确认资源权限。
8. 预览 manifest。
9. 创建 Agnet 部署占位任务。
9. 创建 Agent 部署占位任务。
10. 在客户端持续推进子环节开发与测试。
11. 查看任务状态、日志、审计和交付结果。
当前唯一可延期项:Heicode 到 Agnet 平台真实部署 API 的完整联调。
当前唯一可延期项:Heicode 到 Agent 平台真实部署 API 的完整联调。
+13 -13
View File
@@ -47,7 +47,7 @@ macOS 提示说明:
## 第三步:在客户端输入想法
主要在 Heicode 客户端输入要做的事情。Manager 负责下载、资源绑定、Agnet 部署、状态和审计等辅助能力,不承担主开发对话。
主要在 Heicode 客户端输入要做的事情。Manager 负责下载、资源绑定、Agent 部署、状态和审计等辅助能力,不承担主开发对话。
例如:
@@ -93,12 +93,12 @@ macOS 提示说明:
用户可以使用平台推荐角色:
- Product Agnet。
- Architect Agnet。
- Frontend Agnet。
- Backend Agnet。
- Reviewer Agnet。
- Ops Agnet。
- Product Agent。
- Architect Agent。
- Frontend Agent。
- Backend Agent。
- Reviewer Agent。
- Ops Agent。
每个角色确认:
@@ -120,20 +120,20 @@ macOS 提示说明:
- 访问生产密钥。
- 大额模型预算消耗。
用户确认后,平台才会为子 Agnet 派生短期凭证。
用户确认后,平台才会为子 Agent 派生短期凭证。
## 第七步:启动任务
用户确认任务后,Heicode 会生成:
- 子 Agnet 角色。
- 子 Agent 角色。
- AGENT.md。
- resource context。
- permission manifest。
- 模型和预算配置。
- 审批策略。
Heicode 将这些内容提交给 Agnet 平台执行。
Heicode 将这些内容提交给 Agent 平台执行。
## 第八步:在客户端持续推进
@@ -144,8 +144,8 @@ Heicode 将这些内容提交给 Agnet 平台执行。
```text
客户端继续补充要求
-> Heicode 生成下一步子任务
-> Agnet 执行需求/开发/测试/修复/部署中的当前环节
-> Agnet 按权限调用需要的 SK 工具
-> Agent 执行需求/开发/测试/修复/部署中的当前环节
-> Agent 按权限调用需要的 SK 工具
-> 中间结果和失败原因回到客户端
-> 用户继续修正或审批
-> 最终返回交付物和部署结果
@@ -156,7 +156,7 @@ Heicode 将这些内容提交给 Agnet 平台执行。
用户可以在 Heicode 查看:
- 当前任务状态。
- 子 Agnet 活动状态。
- 子 Agent 活动状态。
- 运行日志。
- 失败原因。
- 模型用量。
+13 -13
View File
@@ -39,8 +39,8 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
- 用户输入自然语言目标。
- Heicode 生成任务、角色、权限和执行上下文。
- 客户端是主体验,Manager 负责资源、部署和审计辅助。
- 子 Agnet 按角色推进开发、测试、修复和部署,并可调用 SK 工具。
- 密钥进入密钥保管器,子 Agnet 只拿短期凭证。
- 子 Agent 按角色推进开发、测试、修复和部署,并可调用 SK 工具。
- 密钥进入密钥保管器,子 Agent 只拿短期凭证。
- Heicode 展示模型用量、日志、事件和审计。
## Slide 4:产品工作流
@@ -55,11 +55,11 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
登录
-> 客户端输入想法
-> Manager 绑定资源
-> Manager 部署 Agnet
-> Manager 部署 Agent
-> 客户端继续推进任务
-> Agnet 执行开发/测试/修复并调用 SK
-> Agent 执行开发/测试/修复并调用 SK
-> 审批高危操作
-> Agnet 交付与部署
-> Agent 交付与部署
-> 观测与维护
```
@@ -75,7 +75,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
1. 想法转开发任务。
2. 资源绑定和权限分配。
3. 子 Agnet 编排和执行。
3. 子 Agent 编排和执行。
4. 用量、日志、安全和审计闭环。
## Slide 6:系统架构
@@ -89,7 +89,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
```text
用户 / 客户端
-> Heicode Manager
-> Agnet 平台
-> Agent 平台
-> CodeGW
-> Azure Key Vault
-> Git / SK / 云资源
@@ -97,7 +97,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
讲述:
客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。Azure Key Vault 是密钥保管器。
客户端是主体验,Manager 是辅助控制台。Agent 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。Azure Key Vault 是密钥保管器。
## Slide 7:安全模型
@@ -109,7 +109,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
- Heicode 服务端数据库只保存 `secret_ref`。
- 长期密钥进入 Azure Key Vault。
- 子 Agnet 只获得短期、最小权限凭证。
- 子 Agent 只获得短期、最小权限凭证。
- 高危操作必须在客户端审批。
- 日志、Markdown、前端响应不包含明文密钥。
@@ -147,7 +147,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
延期项:
- Heicode 到 Agnet 平台真实部署 API 的完整联调。
- Heicode 到 Agent 平台真实部署 API 的完整联调。
## Slide 10:路线图
@@ -160,7 +160,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
1. 资源绑定和密钥保管。
2. CodeGW 用户侧模型和用量展示。
3. 客户端登录、审批和下载闭环。
4. Agnet 平台真实部署联调。
4. Agent 平台真实部署联调。
5. 自动维护、升级、成本和质量观测。
## Slide 11:演示故事
@@ -174,9 +174,9 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
1. 登录 Heicode。
2. 客户端输入产品想法。
3. Manager 绑定 Git 和云资源。
4. Manager 部署 Agnet 并展示权限。
4. Manager 部署 Agent 并展示权限。
5. 客户端继续推进任务。
6. 展示 Agnet 调用 SK 和当前子环节。
6. 展示 Agent 调用 SK 和当前子环节。
7. 客户端审批高危操作。
8. 查看交付、部署结果、日志和模型用量。
@@ -2,14 +2,14 @@
## 核心原则
Heicode 是 SaaS 产品,不能把凭证管理转嫁给用户,也不能让密钥散落在 Git、Markdown、日志或子 Agnet 长期状态中。
Heicode 是 SaaS 产品,不能把凭证管理转嫁给用户,也不能让密钥散落在 Git、Markdown、日志或子 Agent 长期状态中。
安全目标:
1. 用户授权资源,平台托管凭证。
2. Heicode 服务端数据库只保存元数据和 `secret_ref`。
3. 长期密钥进入密钥保管器。
4. 子 Agnet 只拿短期、最小权限、可审计凭证。
4. 子 Agent 只拿短期、最小权限、可审计凭证。
5. 高危操作必须由客户端审批。
## 凭证分类
@@ -43,17 +43,17 @@ Heicode 是 SaaS 产品,不能把凭证管理转嫁给用户,也不能让密
Heicode 接口不得返回明文密钥。日志、错误信息、审计摘要也不得包含明文密钥。
## 子 Agnet 访问凭证
## 子 Agent 访问凭证
子 Agnet 不保存长期密钥。
子 Agent 不保存长期密钥。
推荐流程:
```text
Heicode 生成 Resource Grant
-> Agnet 平台部署子 Agnet
-> Agnet 平台绑定运行时身份
-> 子 Agnet 请求使用资源
-> Agent 平台部署子 Agent
-> Agent 平台绑定运行时身份
-> 子 Agent 请求使用资源
-> 高危操作先走客户端审批
-> 审批通过后派生短期凭证
-> 短期凭证注入运行时
@@ -86,7 +86,7 @@ Heicode 生成 Resource Grant
2. 禁止把密钥写入 Markdown。
3. 禁止前端返回明文密钥。
4. 禁止在日志中打印密钥。
5. 禁止把长期云密钥注入子 Agnet。
5. 禁止把长期云密钥注入子 Agent。
6. 禁止把 Azure Key Vault 管理入口作为普通用户入口暴露。
7. 禁止把 CodeGW key 原文交给客户端长期保存。
@@ -94,7 +94,7 @@ Heicode 生成 Resource Grant
推荐文案:
> Heicode 会把你的授权凭证存入密钥保管器。平台只在你授权的范围内,为对应任务和角色生成短期访问能力。密钥不会写入 Git、文档、日志或子 Agnet 长期状态。
> Heicode 会把你的授权凭证存入密钥保管器。平台只在你授权的范围内,为对应任务和角色生成短期访问能力。密钥不会写入 Git、文档、日志或子 Agent 长期状态。
## 验收标准
@@ -103,6 +103,6 @@ Heicode 生成 Resource Grant
| 数据库 | 只保存 `secret_ref`,不保存明文密钥 |
| 前端 | 不显示明文密钥 |
| 日志 | 不包含 token、password、private key、access key |
| 子 Agnet | 不保存长期凭证 |
| 子 Agent | 不保存长期凭证 |
| 高危操作 | 有客户端审批记录 |
| Azure Key Vault | 普通用户不可直接进入;Manager 仅通过受控后端身份访问 |
@@ -1,8 +1,8 @@
# 07. CodeGW、Azure Key Vault、Agnet 平台边界说明
# 07. CodeGW、Azure Key Vault、Agent 平台边界说明
## 目标
本文用于讲清楚 Heicode 与 CodeGW、Azure Key Vault、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。
本文用于讲清楚 Heicode 与 CodeGW、Azure Key Vault、Agent 平台之间的产品和技术边界,避免再次变成缝合式系统。
## 总体边界
@@ -11,7 +11,7 @@
| Heicode | 产品总入口,包含客户端主体验与 Manager 辅助控制台 | 是 |
| CodeGW | 模型网关、模型调用、余额、额度、用量日志 | 否 |
| Azure Key Vault | 长期凭证托管、密钥引用、后端受控读取 | 否 |
| Agnet 平台 | AKS 执行层、状态、日志、事件、指标 | 否 |
| Agent 平台 | AKS 执行层、状态、日志、事件、指标 | 否 |
## Heicode 与 CodeGW
@@ -51,15 +51,15 @@ Heicode 不做:
产品口径:
> 密钥保管器负责保存凭证,Heicode 只保存引用。子 Agnet 使用资源时,平台按权限和审批结果获取短期访问能力。
> 密钥保管器负责保存凭证,Heicode 只保存引用。子 Agent 使用资源时,平台按权限和审批结果获取短期访问能力。
## Heicode 与 Agnet 平台
## Heicode 与 Agent 平台
Heicode 传给 Agnet 平台:
Heicode 传给 Agent 平台:
- 用户上下文。
- 任务目标。
- 子 Agnet 角色。
- 子 Agent 角色。
- AGENT.md。
- resource context。
- permission manifest。
@@ -69,10 +69,10 @@ Heicode 传给 Agnet 平台:
- `sk_context` / `tool_permissions`。
- 回调地址。
Agnet 平台回传 Heicode:
Agent 平台回传 Heicode:
- deployment 状态。
- 子 Agnet 状态。
- 子 Agent 状态。
- 事件。
- 日志。
- 指标。
@@ -91,7 +91,7 @@ Heicode 不传:
产品口径:
> Heicode 决定用户想做什么、能用什么资源和谁来做;Agnet 平台负责把这些任务安全运行起来,在过程中按权限调用需要的 SK 工具,并把子环节状态、交付物和部署结果回传给 Heicode。
> Heicode 决定用户想做什么、能用什么资源和谁来做;Agent 平台负责把这些任务安全运行起来,在过程中按权限调用需要的 SK 工具,并把子环节状态、交付物和部署结果回传给 Heicode。
## 客户端与 Manager 的边界
@@ -109,7 +109,7 @@ Manager 主做:
- 账号与安全。
- 客户端下载。
- Git、文档、SK、云资源绑定。
- Agnet 部署。
- Agent 部署。
- 任务状态、用量和审计查看。
- 风险与授权范围确认。
@@ -128,7 +128,7 @@ Manager 不做:
## 当前延期项
Heicode 到 Agnet 平台真实部署 API 的完整联调仍可延期。
Heicode 到 Agent 平台真实部署 API 的完整联调仍可延期。
但 Heicode 侧必须准备好:
@@ -145,4 +145,4 @@ Heicode 到 Agnet 平台真实部署 API 的完整联调仍可延期。
2. 客户端只登录 Heicode。
3. CodeGW 独立服务运行。
4. Azure Key Vault 只通过 Manager 后端受控身份访问,普通用户不直接进入。
5. Agnet 平台接口只收结构化上下文和 `secret_ref`,不收长期明文密钥。
5. Agent 平台接口只收结构化上下文和 `secret_ref`,不收长期明文密钥。
+7 -7
View File
@@ -4,7 +4,7 @@
Heicode 客户端是用户主体验,承担本地对话、想法输入、任务推进、执行反馈、交付接收和高危审批。它不是独立模型配置工具,也不是 CodeGW 后台客户端。
Heicode Manager 不是主体验,但它仍然负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态、余额与审计等辅助操作。用户不在网页上编码,主要在客户端里推进开发任务。
Heicode Manager 不是主体验,但它仍然负责账号与安全、客户端下载、Git/云资源绑定、Agent 部署、任务状态、余额与审计等辅助操作。用户不在网页上编码,主要在客户端里推进开发任务。
客户端必须保持:
@@ -41,25 +41,25 @@ Heicode
| 功能 | 说明 |
|------|------|
| 对话输入 | 用户描述产品想法、开发任务或维护需求 |
| 任务推进 | 继续要求、查看 Agnet 执行反馈、接收交付结果 |
| 任务状态 | 查看 Heicode 返回的任务和子 Agnet 进度 |
| 任务推进 | 继续要求、查看 Agent 执行反馈、接收交付结果 |
| 任务状态 | 查看 Heicode 返回的任务和子 Agent 进度 |
| 模型选择 | 从 Heicode 提供的模型中选择 |
| 高危审批 | 对生产部署、密钥访问、云操作做确认 |
| 日志反馈 | 展示关键执行反馈和失败原因 |
| 安装更新 | 从 Heicode 下载页获取安装包 |
## 客户端里的 Agnet 闭环
## 客户端里的 Agent 闭环
客户端里发生的不是单次对话,而是持续推进的任务闭环:
```text
用户输入想法或补充要求
-> Heicode 判断当前子任务
-> 调用 Agnet 执行需求/开发/测试/修复/部署中的某一环
-> Agnet 按权限调用需要的 SK 工具
-> 调用 Agent 执行需求/开发/测试/修复/部署中的某一环
-> Agent 按权限调用需要的 SK 工具
-> 中间结果、失败原因和下一步建议回到客户端
-> 用户继续补充、修正或审批
-> Agnet 继续推进
-> Agent 继续推进
-> 最终返回交付物和部署结果
```
+9 -9
View File
@@ -85,7 +85,7 @@
话术:
> 资源绑定不是把密钥贴到页面里。用户授权后,真实凭证进入密钥保管器,Heicode 只保存 secret_ref。子 Agnet 能看到的是资源元数据和权限,不是长期密钥。
> 资源绑定不是把密钥贴到页面里。用户授权后,真实凭证进入密钥保管器,Heicode 只保存 secret_ref。子 Agent 能看到的是资源元数据和权限,不是长期密钥。
展示:
@@ -97,13 +97,13 @@
操作:
1. 展示推荐子 Agnet 角色。
1. 展示推荐子 Agent 角色。
2. 打开某个角色权限。
3. 展示 manifest 预览。
话术:
> Heicode 会把资源按角色授权。例如 Backend Agnet 可以读写后端路径,Ops Agnet 可以部署测试环境,生产部署需要客户端审批。
> Heicode 会把资源按角色授权。例如 Backend Agent 可以读写后端路径,Ops Agent 可以部署测试环境,生产部署需要客户端审批。
展示:
@@ -134,18 +134,18 @@
操作:
1. 切回客户端,追加一句新要求。
2. 展示当前 Agnet 子环节和 SK 工具调用。
2. 展示当前 Agent 子环节和 SK 工具调用。
3. 展示任务状态、日志、模型用量和审计。
话术:
> 主体验在客户端。用户继续补充要求后,Heicode 会调用 Agnet 推进下一子环节,例如开发、测试或修复。Agnet 在过程中还可以调用已授权的 SK 工具,最后把中间结果、交付物和部署结果回传回来。Manager 负责辅助查看状态、日志、用量和审计。
> 主体验在客户端。用户继续补充要求后,Heicode 会调用 Agent 推进下一子环节,例如开发、测试或修复。Agent 在过程中还可以调用已授权的 SK 工具,最后把中间结果、交付物和部署结果回传回来。Manager 负责辅助查看状态、日志、用量和审计。
展示:
- 客户端追加需求。
- 当前 Agnet 子环节和 SK 工具调用。
- 子 Agnet 状态。
- 当前 Agent 子环节和 SK 工具调用。
- 子 Agent 状态。
- 日志。
- 用量。
- 审计记录。
@@ -168,10 +168,10 @@
2. 不进入 CodeGW 管理后台。
3. 不展示 Azure Key Vault access token。
4. 不强调底层配置复杂度,强调用户流程。
5. 如果 Agnet 真实部署尚未联调,明确说当前展示的是 Heicode 侧 payload、manifest 和占位控制面。
5. 如果 Agent 真实部署尚未联调,明确说当前展示的是 Heicode 侧 payload、manifest 和占位控制面。
## 备用 1 分钟版本
话术:
> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 Azure Key Vault,只在 Heicode 和客户端完成整个开发流程。
> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agent。Heicode 生成子 Agent 团队、角色和权限,密钥进入密钥保管器,子 Agent 只拿短期凭证。Agent 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 Azure Key Vault,只在 Heicode 和客户端完成整个开发流程。
@@ -177,7 +177,7 @@ Heicode 只追问影响执行路径的少量问题:
| 区块 | 内容 |
|------|------|
| 当前子环节 | 需求、设计、后端、前端、检查、测试、部署的阶段状态 |
| Agnet 调用 | 当前角色、正在调用的 SK 工具、工具输出摘要 |
| Agent 调用 | 当前角色、正在调用的 SK 工具、工具输出摘要 |
| 最新动态 | Heicode 翻译后的关键执行事件 |
| 当前产物 | 产品文档、接口草案、代码变更、检查报告 |
| 继续输入 | 用户继续追加需求或修正方向 |
@@ -190,7 +190,7 @@ Heicode 只追问影响执行路径的少量问题:
- 失败必须展示可理解原因和建议动作。
- 停止任务要说明影响范围。
- 当前子环节必须可见,不能只显示“运行中”。
- 如果 Agnet 调用了 SK 工具,前端要展示“调用了什么、产出了什么、是否失败”。
- 如果 Agent 调用了 SK 工具,前端要展示“调用了什么、产出了什么、是否失败”。
- 交付和部署结果必须回到客户端任务空间,而不是只留在后台日志里。
## 任务用量与审计
@@ -7,19 +7,19 @@
核心边界:
- Heicode 客户端是主体验。用户主要在客户端里对话、描述想法、推进开发任务、查看执行反馈和审批高危操作。
- Heicode Manager / 浏览器控制台是辅助控制台。它负责账号与安全、客户端下载、Git 绑定、云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看。
- Heicode Manager / 浏览器控制台是辅助控制台。它负责账号与安全、客户端下载、Git 绑定、云资源绑定、Agent 部署、任务状态总览、模型余额与用量、审计与日志查看。
- 用户不在网页上编码,也不在网页上完成主要开发对话。
- 用户会在 Manager 里做辅助操作和观测,但主开发体验仍然在客户端。
- CodeGW、密钥保管器、Agnet 平台后台都不是普通用户入口,只作为 Heicode 的底层能力。
- CodeGW、密钥保管器、Agent 平台后台都不是普通用户入口,只作为 Heicode 的底层能力。
## 原型范围
| 范围 | 是否属于本文原型 | 说明 |
|------|------------------|------|
| Heicode 客户端 | 是,主体验 | 本地对话、想法输入、任务推进、执行反馈、高危审批 |
| Heicode Manager / 浏览器控制台 | 是,辅助控制台 | 账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务总览、余额与审计 |
| Heicode Manager / 浏览器控制台 | 是,辅助控制台 | 账号与安全、客户端下载、Git/云资源绑定、Agent 部署、任务总览、余额与审计 |
| 网页编码界面 | 否 | 用户不在网页上编码 |
| 底层服务后台 | 否 | CodeGW、密钥保管器、Agnet 平台后台不直接暴露给普通用户 |
| 底层服务后台 | 否 | CodeGW、密钥保管器、Agent 平台后台不直接暴露给普通用户 |
## 原型原则
@@ -27,8 +27,8 @@
2. Manager 承载资源准备、部署准备、任务总览、用量和审计等辅助能力。
3. 主开发对话、编码推进和高危审批仍然以客户端为主。
4. 高危动作必须在客户端清晰审批。
5. 继续开发时,Heicode 会持续调用 Agnet 推进需求、开发、测试、修复和部署等子环节。
6. Agnet 执行过程中可以按权限调用 SK 工具。
5. 继续开发时,Heicode 会持续调用 Agent 推进需求、开发、测试、修复和部署等子环节。
6. Agent 执行过程中可以按权限调用 SK 工具。
7. Manager 可以查看任务状态、用量和审计,但不把网页设计成 IDE 或网页编码工作台。
8. 客户端只登录 Heicode,只使用 Heicode 提供的模型。
@@ -39,8 +39,8 @@ flowchart LR
Client["Heicode 客户端主体验"] --> Idea["输入想法 / 继续开发"]
Idea --> Task["生成任务卡"]
Task --> Manager["Manager 准备资源 / 查看辅助信息"]
Manager --> Deploy["Manager 部署 Agnet"]
Deploy --> Loop["Agnet 子环节循环"]
Manager --> Deploy["Manager 部署 Agent"]
Deploy --> Loop["Agent 子环节循环"]
Loop --> Dev["需求 / 开发 / 测试 / 修复"]
Dev --> SK["按权限调用 SK 工具"]
SK --> Run["执行反馈和对话"]
@@ -106,7 +106,7 @@ flowchart LR
│ │ 后端实现中 │ │ 等待生产部署审批 │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ │
│ 辅助:打开 Manager 绑定资源、部署 Agnet、查看状态 │
│ 辅助:打开 Manager 绑定资源、部署 Agent、查看状态 │
└──────────────────────────────────────────────────────────────────────────────┘
```
@@ -162,7 +162,7 @@ Heicode 在客户端里追问关键缺口,并生成任务卡。
│ 产品说明 / 原型描述 / 开发任务 / 检查清单 / 部署步骤 │
│ │
│ 需要 Manager 辅助确认 │
│ [绑定 Git] [绑定云资源] [部署 Agnet] [查看预算与审计] │
│ [绑定 Git] [绑定云资源] [部署 Agent] [查看预算与审计] │
│ │
│ [修改目标] [去 Manager 准备] │
└──────────────────────────────────────────────────────────────────────────────┘
@@ -188,10 +188,10 @@ Manager 是网页端辅助控制台,不是编码主体验,但依然承载资
│ │ ¥128.40 / 今日 ¥12.30 │ │ 1 个高危审批待处理 │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Agnet 部署 │ │
│ │ Agent 部署 │ │
│ │ 角色:Product / Backend / Frontend / Reviewer / Ops │ │
│ │ 状态:待部署 │ │
│ │ [部署 Agnet] │ │
│ │ [部署 Agent] │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 你可以在这里做辅助操作: │
@@ -209,7 +209,7 @@ Manager 是网页端辅助控制台,不是编码主体验,但依然承载资
## 5. Manager 绑定 Git 和云资源
Git 和云资源在 Manager 中绑定,作为部署 Agnet 的前置条件。
Git 和云资源在 Manager 中绑定,作为部署 Agent 的前置条件。
```text
┌──────────────────────────────────────────────────────────────────────────────┐
@@ -229,7 +229,7 @@ Git 和云资源在 Manager 中绑定,作为部署 Agnet 的前置条件。
│ │ [选择本任务允许使用的资源] │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
│ [保存资源绑定] [继续部署 Agnet] │
│ [保存资源绑定] [继续部署 Agent] │
└──────────────────────────────────────────────────────────────────────────────┘
```
@@ -237,7 +237,7 @@ Git 和云资源在 Manager 中绑定,作为部署 Agnet 的前置条件。
- Git 和云资源绑定属于 Manager。
- 用户不在网页上写代码。
- 绑定完成后可以继续部署 Agnet,也可以返回查看任务状态与辅助信息。
- 绑定完成后可以继续部署 Agent,也可以返回查看任务状态与辅助信息。
## 6. Manager 云资源自动发现
@@ -261,11 +261,11 @@ Git 和云资源在 Manager 中绑定,作为部署 Agnet 的前置条件。
└──────────────────────────────────────────────────────────────────────────────┘
```
## 7. Manager 部署 Agnet
## 7. Manager 部署 Agent
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Manager / 部署 Agnet │
│ Heicode Manager / 部署 Agent │
├──────────────────────────────────────────────────────────────────────────────┤
│ 任务:小团队任务管理 SaaS │
│ │
@@ -273,7 +273,7 @@ Git 和云资源在 Manager 中绑定,作为部署 Agnet 的前置条件。
│ - Git:repo-main / main │
│ - 云资源:aks-test、db-dev │
│ │
│ Agnet 角色 │
│ Agent 角色 │
│ [x] Product 需求和验收 │
│ [x] Backend API / 数据模型 │
│ [x] Frontend 页面和交互 │
@@ -283,30 +283,30 @@ Git 和云资源在 Manager 中绑定,作为部署 Agnet 的前置条件。
│ 安全边界 │
│ - 生产部署需要客户端审批 │
│ - 生产数据默认禁止 │
│ - 长期密钥不下发给 Agnet │
│ - 长期密钥不下发给 Agent │
│ │
│ [返回修改资源] [部署 Agnet] │
│ [返回修改资源] [部署 Agent] │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 设计意图
- Manager 的最终动作是部署 Agnet。
- Manager 的最终动作是部署 Agent。
- Manager 不继续承担开发任务执行。
- 部署完成后主流程回到客户端,但状态和审计仍可在 Manager 查看。
## 8. 客户端执行空间
Agnet 部署完成后,主流程回到客户端。
Agent 部署完成后,主流程回到客户端。
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ Heicode Client / 小团队任务管理 SaaS 运行中 ¥12.30 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Heicode:Agnet 已部署,我开始推进当前开发任务。 │
│ Heicode:Agent 已部署,我开始推进当前开发任务。 │
│ │
│ 当前 Agnet 子环节 │
│ 当前 Agent 子环节 │
│ - 需求和原型:已完成 │
│ - 后端开发:进行中 │
│ - 前端开发:等待接口确认 │
@@ -325,15 +325,15 @@ Agnet 部署完成后,主流程回到客户端。
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ [发送] │
│ │
│ 辅助查看:打开 Manager 查看 Git/云资源绑定、Agnet 部署、用量和审计 │
│ 辅助查看:打开 Manager 查看 Git/云资源绑定、Agent 部署、用量和审计 │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 设计意图
- 主执行反馈在客户端。
- 用户看到的是 Agnet 持续推进的子环节,而不是一次性执行结果。
- Agnet 可以在执行过程中调用已授权的 SK 工具。
- 用户看到的是 Agent 持续推进的子环节,而不是一次性执行结果。
- Agent 可以在执行过程中调用已授权的 SK 工具。
- Manager 仍然是资源、部署、状态、余额和审计的辅助入口。
- 用户持续通过自然语言推进任务。
@@ -363,7 +363,7 @@ Agnet 部署完成后,主流程回到客户端。
│ Heicode Client / 小团队任务管理 SaaS 已完成 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Heicode:第一版已完成,Agnet 已整理交付物并完成目标环境部署。 │
│ Heicode:第一版已完成,Agent 已整理交付物并完成目标环境部署。 │
│ │
│ 交付物 │
│ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │
@@ -376,7 +376,7 @@ Agnet 部署完成后,主流程回到客户端。
│ - 安全检查:1 个问题已修复 │
│ - 部署:测试环境成功,生产环境待你确认后可继续 │
│ │
│ [继续迭代] [发起维护任务] [查看 Agnet 部署信息] │
│ [继续迭代] [发起维护任务] [查看 Agent 部署信息] │
└──────────────────────────────────────────────────────────────────────────────┘
```
@@ -424,12 +424,12 @@ Agnet 部署完成后,主流程回到客户端。
客户端说目标
-> Heicode 生成任务
-> Manager 绑定 Git 和云资源
-> Manager 部署 Agnet
-> Manager 部署 Agent
-> 客户端持续追加需求和修正方向
-> Agnet 执行需求、开发、测试、修复等子环节
-> Agnet 按权限调用 SK 工具
-> Agent 执行需求、开发、测试、修复等子环节
-> Agent 按权限调用 SK 工具
-> 客户端接收中间结果并审批高危动作
-> Agnet 完成交付整理和部署
-> Agent 完成交付整理和部署
-> Manager 提供状态、余额、下载和审计辅助
-> 客户端接收交付结果
```
@@ -441,8 +441,8 @@ flowchart TD
ClientHome["客户端输入想法"] --> ClientAsk["客户端追问缺口"]
ClientAsk --> Task["客户端任务卡"]
Task --> Manager["Manager 绑定资源 / 查看状态"]
Manager --> Deploy["Manager 部署 Agnet"]
Deploy --> Loop["Agnet 子环节循环"]
Manager --> Deploy["Manager 部署 Agent"]
Deploy --> Loop["Agent 子环节循环"]
Loop --> ClientRun["客户端执行空间"]
Loop --> SK["SK 工具调用"]
SK --> ClientRun
@@ -2,7 +2,7 @@
## 使用场景
本文用于产品演示、培训、录屏旁白和销售讲解。语气以真实用户操作为主,不讲复杂技术实现;当需要解释安全、模型、密钥和 Agnet 平台时,只讲用户需要理解的边界。
本文用于产品演示、培训、录屏旁白和销售讲解。语气以真实用户操作为主,不讲复杂技术实现;当需要解释安全、模型、密钥和 Agent 平台时,只讲用户需要理解的边界。
## 一句话开场
@@ -81,7 +81,7 @@ https://code.xinghanlab.com
- 第一版包含哪些功能。
- 推荐哪些页面和接口。
- 需要哪些代码仓库、文档、SK 和云资源。
- 建议使用哪些子 Agnet 角色。
- 建议使用哪些子 Agent 角色。
- 哪些操作可能属于高危操作。
如果草案不准确,我可以直接修改。确认后,Heicode 会提示我补充这个任务需要的上下文。
@@ -105,7 +105,7 @@ https://code.xinghanlab.com
如果我有产品文档、原型描述或技能仓库,我继续绑定项目文档和 SK。
项目文档可以作为 Product Agnet 和 Architect Agnet 的上下文。SK 可以作为子 Agnet 可调用的技能来源。
项目文档可以作为 Product Agent 和 Architect Agent 的上下文。SK 可以作为子 Agent 可调用的技能来源。
绑定后,平台知道:
@@ -154,18 +154,18 @@ https://code.xinghanlab.com
我不会看到明文密钥。平台最多展示 `secret_ref` 这种引用,让我知道凭证由密钥保管器管理。
### 12. 生成子 Agnet 团队
### 12. 生成子 Agent 团队
资源准备好后,Heicode 会推荐一组子 Agnet 角色。
资源准备好后,Heicode 会推荐一组子 Agent 角色。
例如:
- Product Agnet:负责需求和验收标准。
- Architect Agnet:负责架构和技术方案。
- Frontend Agnet:负责页面和交互。
- Backend Agnet:负责 API、数据模型和服务逻辑。
- Reviewer Agnet:负责代码检查和安全审查。
- Ops Agnet:负责构建、部署和运行观察。
- Product Agent:负责需求和验收标准。
- Architect Agent:负责架构和技术方案。
- Frontend Agent:负责页面和交互。
- Backend Agent:负责 API、数据模型和服务逻辑。
- Reviewer Agent:负责代码检查和安全审查。
- Ops Agent:负责构建、部署和运行观察。
我可以使用推荐团队,也可以增减角色。
@@ -175,18 +175,18 @@ https://code.xinghanlab.com
例如:
Backend Agnet 可以:
Backend Agent 可以:
- 读项目文档。
- 读写后端代码路径。
- 使用开发数据库。
Frontend Agnet 可以:
Frontend Agent 可以:
- 读项目文档。
- 读写前端代码路径。
Ops Agnet 可以:
Ops Agent 可以:
- 部署测试环境。
- 请求生产部署审批。
@@ -212,8 +212,8 @@ manifest 不会显示 Git token、云 access key、SSH 私钥、数据库密码
在开始执行前,Heicode 会用一张确认卡展示本次任务摘要:
- 本次要做什么。
- 会启动哪些子 Agnet。
- 每个子 Agnet 使用哪些资源。
- 会启动哪些子 Agent。
- 每个子 Agent 使用哪些资源。
- 是否会访问密钥。
- 是否会部署云资源。
- 预计模型预算。
@@ -228,7 +228,7 @@ manifest 不会显示 Git token、云 access key、SSH 私钥、数据库密码
例如:
```text
Ops Agnet 请求部署到生产环境。
Ops Agent 请求部署到生产环境。
目标资源:aks-prod
操作:cloud:deploy
风险等级:高
@@ -237,7 +237,7 @@ Ops Agnet 请求部署到生产环境。
这时我可以批准或拒绝。
如果我批准,平台会使用密钥保管器派生短期、最小权限凭证。子 Agnet 不会拿到长期密钥。
如果我批准,平台会使用密钥保管器派生短期、最小权限凭证。子 Agent 不会拿到长期密钥。
### 17. 在客户端持续推进开发
@@ -248,11 +248,11 @@ Ops Agnet 请求部署到生产环境。
```text
我在客户端补充要求
-> Heicode 判断下一步要推进哪个子环节
-> Agnet 执行需求、开发、测试、修复或部署中的当前任务
-> Agnet 按权限调用已绑定的 SK 工具
-> Agent 执行需求、开发、测试、修复或部署中的当前任务
-> Agent 按权限调用已绑定的 SK 工具
-> 中间结果回到客户端
-> 我继续修正方向或批准高危动作
-> Agnet 继续推进直到交付和部署完成
-> Agent 继续推进直到交付和部署完成
```
### 18. 查看执行状态
@@ -261,11 +261,11 @@ Ops Agnet 请求部署到生产环境。
我能看到:
- Product Agnet 是否完成需求。
- Backend Agnet 是否正在写代码。
- Frontend Agnet 是否等待后端接口。
- Reviewer Agnet 是否发现问题。
- Ops Agnet 是否等待审批或部署中。
- Product Agent 是否完成需求。
- Backend Agent 是否正在写代码。
- Frontend Agent 是否等待后端接口。
- Reviewer Agent 是否发现问题。
- Ops Agent 是否等待审批或部署中。
如果失败,页面应该显示失败原因,而不是只显示一个错误码。
@@ -275,7 +275,7 @@ Ops Agnet 请求部署到生产环境。
这里能看到:
- 子 Agnet 运行日志。
- 子 Agent 运行日志。
- 模型调用日志。
- 消耗了多少额度。
- 哪个任务使用了哪个模型。
@@ -311,15 +311,15 @@ Ops Agnet 请求部署到生产环境。
并检查现有权限模型是否支持团队成员邀请。
```
Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生成新的开发任务。
Heicode 会复用之前的项目上下文、资源绑定和 Agent 角色,生成新的开发任务。
## 简短演示口述版
> 我先登录 Heicode 的浏览器控制台。这里不是 CodeGW 后台,而是资源、部署、状态和审计的辅助入口。登录后,我会在客户端输入一个产品想法,比如做一个小团队任务管理 SaaS。平台会生成需求草案、推荐资源和子 Agnet 团队。接下来我绑定 Git 仓库、项目文档、SK 和云资源。真实密钥会进入密钥保管器,Heicode 只保存 secret_ref。然后我确认每个子 Agnet 的角色和权限,比如 Backend 能写后端路径,Ops 可以部署测试环境,但生产部署需要客户端审批。任务运行后,Agnet 会持续推进开发、测试和部署,并按权限调用需要的 SK 工具;我可以看到每个子 Agnet 的状态、日志、模型用量和审计记录。客户端只登录 code.xinghanlab.com,不选择模型提供方,模型由 Heicode 提供。整个流程的重点是:从一个想法开始,把产品、代码、云资源、模型、密钥和审计组织成一个完整的软件生命周期。
> 我先登录 Heicode 的浏览器控制台。这里不是 CodeGW 后台,而是资源、部署、状态和审计的辅助入口。登录后,我会在客户端输入一个产品想法,比如做一个小团队任务管理 SaaS。平台会生成需求草案、推荐资源和子 Agent 团队。接下来我绑定 Git 仓库、项目文档、SK 和云资源。真实密钥会进入密钥保管器,Heicode 只保存 secret_ref。然后我确认每个子 Agent 的角色和权限,比如 Backend 能写后端路径,Ops 可以部署测试环境,但生产部署需要客户端审批。任务运行后,Agent 会持续推进开发、测试和部署,并按权限调用需要的 SK 工具;我可以看到每个子 Agent 的状态、日志、模型用量和审计记录。客户端只登录 code.xinghanlab.com,不选择模型提供方,模型由 Heicode 提供。整个流程的重点是:从一个想法开始,把产品、代码、云资源、模型、密钥和审计组织成一个完整的软件生命周期。
## 极简销售口述版
> Heicode 让用户从一个想法开始,接入自己的代码和云资源,由 Heicode 生成 AI 开发团队。平台负责资源授权、密钥保管、模型用量和审计;子 Agnet 负责开发、检查和部署。用户只在 Heicode 和客户端中操作,不需要进入 CodeGW 后台,也不会把长期密钥交给子 Agnet。
> Heicode 让用户从一个想法开始,接入自己的代码和云资源,由 Heicode 生成 AI 开发团队。平台负责资源授权、密钥保管、模型用量和审计;子 Agent 负责开发、检查和部署。用户只在 Heicode 和客户端中操作,不需要进入 CodeGW 后台,也不会把长期密钥交给子 Agent。
## 口述时必须强调
@@ -328,7 +328,7 @@ Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生
3. CodeGW 是内部模型网关,不是普通用户后台。
4. 密钥进入密钥保管器,不进入 Git、Markdown 或日志。
5. 高危操作在客户端审批。
6. 子 Agnet 拿短期凭证,不拿长期密钥。
6. 子 Agent 拿短期凭证,不拿长期密钥。
7. 资源绑定和权限分配要尽量自动化和角色化。
## 口述时不要说
@@ -338,4 +338,4 @@ Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生
3. 让客户端选择模型提供方。
4. 让用户手写复杂 manifest。
5. 把 tenant/project 当作当前产品主轴。
6. 把 Agnet 平台说成用户控制台。
6. 把 Agent 平台说成用户控制台。
+39 -39
View File
@@ -14,13 +14,13 @@ Heicode 平台由五个主要部分组成:
| 部分 | 用户理解 | 平台职责 |
|------|----------|----------|
| Heicode Manager | 浏览器辅助控制台 | 账号与安全、客户端下载、绑定资源、部署 Agnet、查看状态、余额和审计 |
| Heicode Manager | 浏览器辅助控制台 | 账号与安全、客户端下载、绑定资源、部署 Agent、查看状态、余额和审计 |
| Heicode 客户端 | 本地主体验和审批入口 | 登录 Heicode、输入任务、继续开发、查看反馈、接收交付结果、审批高危操作 |
| Agnet 平台 | AI 开发团队执行层 | 在 AKS 上运行子 Agnet,回传状态、日志、事件和指标 |
| Agent 平台 | AI 开发团队执行层 | 在 AKS 上运行子 Agent,回传状态、日志、事件和指标 |
| CodeGW | 模型与用量底座 | 提供模型调用、余额、额度、用量和调用日志 |
| 密钥保管器 | 凭证托管服务 | 保存长期密钥,按审批和权限生成短期访问能力 |
用户主要使用客户端和 Manager。CodeGW、密钥保管器和 Agnet 平台是底层能力,不作为普通用户日常操作后台。
用户主要使用客户端和 Manager。CodeGW、密钥保管器和 Agent 平台是底层能力,不作为普通用户日常操作后台。
## 二、平台适合谁
@@ -62,34 +62,34 @@ Heicode 平台由五个主要部分组成:
### 3. AI 开发团队生成
平台根据任务目标推荐子 Agnet 角色。
平台根据任务目标推荐子 Agent 角色。
常见角色:
- Product Agnet。
- Architect Agnet。
- Frontend Agnet。
- Backend Agnet。
- Reviewer Agnet。
- Ops Agnet。
- Product Agent。
- Architect Agent。
- Frontend Agent。
- Backend Agent。
- Reviewer Agent。
- Ops Agent。
每个子 Agnet 都有明确职责、可用资源、权限范围、模型配置和审计记录。
每个子 Agent 都有明确职责、可用资源、权限范围、模型配置和审计记录。
### 3.1 Agnet 持续执行闭环
### 3.1 Agent 持续执行闭环
Heicode 不是把任务分发给 Agnet 一次就结束,而是在继续开发中持续调用 Agnet 完成子环节。
Heicode 不是把任务分发给 Agent 一次就结束,而是在继续开发中持续调用 Agent 完成子环节。
完整闭环应表达为:
```text
客户端输入想法或追加需求
-> Manager 准备资源和部署条件
-> Heicode 调度 Agnet 执行当前子环节
-> Agnet 完成需求/开发/测试/修复/部署中的一步
-> Agnet 按权限调用 SK 工具
-> Heicode 调度 Agent 执行当前子环节
-> Agent 完成需求/开发/测试/修复/部署中的一步
-> Agent 按权限调用 SK 工具
-> 中间结果和状态回传给客户端与 Manager
-> 用户继续补充、修正或审批
-> 最终由 Agnet 完成交付整理和部署
-> 最终由 Agent 完成交付整理和部署
```
### 4. 权限分配
@@ -99,7 +99,7 @@ Heicode 使用 Resource Grant 描述“哪个角色能用哪个资源、能做
示例:
```text
Backend Agnet
Backend Agent
-> 可读项目文档
-> 可读写后端代码路径
-> 可访问开发数据库
@@ -120,7 +120,7 @@ Backend Agnet
- 访问生产密钥。
- 大额模型预算消耗。
审批通过后,平台可以从密钥保管器派生短期、最小权限凭证给子 Agnet 使用。
审批通过后,平台可以从密钥保管器派生短期、最小权限凭证给子 Agent 使用。
### 6. 模型与余额
@@ -152,7 +152,7 @@ Heicode 统一展示任务执行、模型调用、资源访问、高危审批和
谁
在什么时候
为了哪个任务
让哪个子 Agnet
让哪个子 Agent
使用了哪个资源
执行了什么操作
是否经过审批
@@ -161,7 +161,7 @@ Heicode 统一展示任务执行、模型调用、资源访问、高危审批和
### 8. 部署和维护
Agnet 平台负责执行开发、检查、测试、修复和部署任务。Heicode 展示部署状态、失败原因、日志、指标和审计。
Agent 平台负责执行开发、检查、测试、修复和部署任务。Heicode 展示部署状态、失败原因、日志、指标和审计。
上线后,用户可以继续输入维护和升级需求,平台复用已有项目上下文、资源绑定和权限规则。
@@ -175,14 +175,14 @@ Agnet 平台负责执行开发、检查、测试、修复和部署任务。Heico
-> 客户端输入产品想法
-> 查看任务草案
-> 绑定 Git / 文档 / SK / 云资源
-> 生成子 Agnet 团队
-> 生成子 Agent 团队
-> 分配角色权限
-> 预览 permission manifest
-> 确认部署计划
-> Manager 部署 Agnet
-> Manager 部署 Agent
-> 客户端持续推进任务
-> 客户端审批高危操作
-> Agnet 平台执行任务并调用 SK
-> Agent 平台执行任务并调用 SK
-> Heicode 查看日志、用量、审计和交付结果
-> 后续维护和升级
```
@@ -198,7 +198,7 @@ Heicode Manager 是浏览器辅助控制台。
- 登录和查看首页。
- 绑定资源。
- 分配角色和权限。
- 创建或部署 Agnet 任务。
- 创建或部署 Agent 任务。
- 查看模型和余额。
- 查看日志与审计。
- 下载客户端。
@@ -225,20 +225,20 @@ Manager 不应该变成 CodeGW 后台,也不应该要求用户直接管理底
- 直接访问密钥保管器。
- 保存长期云密钥。
### 5.3 Agnet 平台
### 5.3 Agent 平台
Agnet 平台是运行时执行层。
Agent 平台是运行时执行层。
它负责:
- 在 AKS 上部署子 Agnet。
- 维护子 Agnet 状态。
- 在 AKS 上部署子 Agent。
- 维护子 Agent 状态。
- 执行开发、检查、测试、修复、部署和维护任务。
- 在执行过程中按权限调用 SK 工具。
- 回传日志、事件、指标和审计数据。
- 按 Resource Grant 和运行时身份访问资源。
普通用户不直接操作 Agnet 平台后台,而是在 Heicode 中查看 Agnet 状态和结果。
普通用户不直接操作 Agent 平台后台,而是在 Heicode 中查看 Agent 状态和结果。
### 5.4 CodeGW
@@ -312,7 +312,7 @@ Heicode 判断当前任务需要哪些上下文
- 调用日志。
- 失败日志。
子 Agnet 的运行模型属于 Agnet 平台部署配置,不等同于 CodeGW 后台的模型供应商配置。
子 Agent 的运行模型属于 Agent 平台部署配置,不等同于 CodeGW 后台的模型供应商配置。
## 九、密钥保管说明
@@ -322,9 +322,9 @@ Heicode 判断当前任务需要哪些上下文
- Markdown。
- 前端响应。
- 普通日志。
- 子 Agnet 长期状态。
- 子 Agent 长期状态。
长期密钥进入密钥保管器。Heicode 保存 `secret_ref`。子 Agnet 使用资源时,只获取短期、最小权限、可审计凭证。
长期密钥进入密钥保管器。Heicode 保存 `secret_ref`。子 Agent 使用资源时,只获取短期、最小权限、可审计凭证。
## 十、平台边界
@@ -337,7 +337,7 @@ Heicode 判断当前任务需要哪些上下文
- CodeGW 后台。
- Azure Key Vault 控制台。
- Agnet 平台后台。
- Agent 平台后台。
用户不应该看到:
@@ -355,7 +355,7 @@ Heicode 判断当前任务需要哪些上下文
### 2. 我必须绑定 Git 吗?
如果要让子 Agnet 修改代码,建议绑定 Git 仓库。只做产品文档或原型描述时,可以先不绑定代码仓库。
如果要让子 Agent 修改代码,建议绑定 Git 仓库。只做产品文档或原型描述时,可以先不绑定代码仓库。
### 3. 我必须绑定云资源吗?
@@ -363,7 +363,7 @@ Heicode 判断当前任务需要哪些上下文
### 4. 密钥安全吗?
平台设计上不把明文密钥保存到 Heicode 服务端数据库、Git、Markdown、日志或前端响应中。真实凭证进入密钥保管器,子 Agnet 只使用短期、最小权限凭证。
平台设计上不把明文密钥保存到 Heicode 服务端数据库、Git、Markdown、日志或前端响应中。真实凭证进入密钥保管器,子 Agent 只使用短期、最小权限凭证。
### 5. 客户端必须安装吗?
@@ -373,9 +373,9 @@ Heicode 判断当前任务需要哪些上下文
CodeGW 是 Heicode 的模型网关和计费服务。普通用户不需要进入 CodeGW 后台,只在 Heicode 中查看模型、余额、额度和调用日志。
### 7. Agnet 是什么?
### 7. Agent 是什么?
Agnet 是平台生成的 AI 开发角色。不同 Agnet 负责不同任务,例如产品、架构、前端、后端、审查和部署。
Agent 是平台生成的 AI 开发角色。不同 Agent 负责不同任务,例如产品、架构、前端、后端、审查和部署。
### 8. 生产部署怎么保证安全?
@@ -391,4 +391,4 @@ Agnet 是平台生成的 AI 开发角色。不同 Agnet 负责不同任务,例
## 十二、平台说明标准话术
> Heicode 是一个从想法到上线的软件生命周期平台。用户在 Heicode 中输入产品想法,绑定代码、文档、SK 和云资源。Heicode 生成子 Agnet 团队和权限清单,客户端负责高危审批,Agnet 平台负责执行开发和部署,CodeGW 提供模型和用量,密钥保管器保存长期凭证。用户只需要使用 Heicode 和客户端,不需要直接进入 CodeGW 后台或密钥保管器后台。
> Heicode 是一个从想法到上线的软件生命周期平台。用户在 Heicode 中输入产品想法,绑定代码、文档、SK 和云资源。Heicode 生成子 Agent 团队和权限清单,客户端负责高危审批,Agent 平台负责执行开发和部署,CodeGW 提供模型和用量,密钥保管器保存长期凭证。用户只需要使用 Heicode 和客户端,不需要直接进入 CodeGW 后台或密钥保管器后台。
+7 -7
View File
@@ -14,11 +14,11 @@
| [`04-platform-usage-guide.md`](./04-platform-usage-guide.md) | 登录平台和使用平台指南,面向真实用户上手 |
| [`05-ppt-copy.md`](./05-ppt-copy.md) | 产品 PPT 文案,面向路演、客户介绍和团队同步 |
| [`06-security-and-credential-management.md`](./06-security-and-credential-management.md) | 安全与凭证管理说明,解释密钥保管器、`secret_ref`、高危审批 |
| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、Azure Key Vault、Agnet 平台边界说明 |
| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、Azure Key Vault、Agent 平台边界说明 |
| [`08-client-guide.md`](./08-client-guide.md) | Heicode 客户端说明,强调只登录 Heicode、只使用 Heicode 模型 |
| [`09-demo-script.md`](./09-demo-script.md) | 5 分钟演示脚本,用于产品演示和录屏 |
| [`10-frontend-detail-spec.md`](./10-frontend-detail-spec.md) | 前端细节说明,覆盖 Heicode 和客户端页面、组件、状态、文案规则 |
| [`11-product-prototype-wireframes.md`](./11-product-prototype-wireframes.md) | 产品原型图,说明 Heicode 客户端主体验、Manager 辅助控制台与 Agnet 执行闭环的边界 |
| [`11-product-prototype-wireframes.md`](./11-product-prototype-wireframes.md) | 产品原型图,说明 Heicode 客户端主体验、Manager 辅助控制台与 Agent 执行闭环的边界 |
| [`12-narrated-user-operation-flow.md`](./12-narrated-user-operation-flow.md) | 口述用户操作流程,用于演示、培训、录屏旁白和销售讲解 |
| [`13-platform-description.md`](./13-platform-description.md) | 平台说明文档,解释平台组成、使用方式、系统边界和常见问题 |
@@ -34,15 +34,15 @@ Heicode 是一款面向全流程智能开发的 SaaS Code 工具。用户注册
- 用户侧叫 `Heicode`,不要叫 CodeGW 后台。
- `Heicode 客户端` 是用户主体验,用于对话、输入想法、推进开发任务、查看执行反馈、高危审批和接收交付结果。
- `Heicode Manager` 是浏览器里的辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态、余额与审计。
- 继续开发时,Heicode 会持续调用 Agnet 推进需求、开发、测试、修复和部署等子环节。
- Agnet 在执行过程中可以调用已授权的 SK 工具,最终完成交付与部署并把结果回传给客户端和 Manager。
- `Heicode Manager` 是浏览器里的辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agent 部署、任务状态、余额与审计。
- 继续开发时,Heicode 会持续调用 Agent 推进需求、开发、测试、修复和部署等子环节。
- Agent 在执行过程中可以调用已授权的 SK 工具,最终完成交付与部署并把结果回传给客户端和 Manager。
- 用户侧叫 `密钥保管器`,技术实现可以说明是 Azure Key Vault;Manager 通过 VM Managed Identity 访问,不要求普通用户进入 Key Vault 后台。
- 用户侧叫 `资源绑定`,不要叫 Git 来源。
- 客户端不出现 `模型提供方`、`旧服务入口`、`第三方路由入口`。
- CodeGW 是内部模型网关和计费服务,不作为普通用户产品入口。
- Agnet 平台是执行层,不替代 Heicode 的用户控制台。
- Agent 平台是执行层,不替代 Heicode 的用户控制台。
## 当前产品边界
当前资料包以 `docs/heicode.md`、`docs/plan.md`、`docs/product-requirements.md` 为依据。旧 Agnet 草案、旧 M1-M5 计划和旧 UI 命名不作为产品主线。
当前资料包以 `docs/heicode.md`、`docs/plan.md`、`docs/product-requirements.md` 为依据。旧 Agent 草案、旧 M1-M5 计划和旧 UI 命名不作为产品主线。
+92
View File
@@ -0,0 +1,92 @@
# Heicode Manager 本地交接敏感配置填写表
# 更新时间:2026-06-01
#
# 说明:
# 1. 这个文件只放在本地交接目录,不要提交 git。
# 2. 我不能凭空写入无法读取/无法确认的明文密码;下面列出所有需要手动取值的项。
# 3. 真实值建议从生产 VM .env、docker inspect、Azure Portal、NewAPI 后台或负责人安全渠道获取。
# 4. 填完后只用于人工交接,不要发到公开聊天、截图、Markdown 或仓库。
### 1. VM SSH
HEICODE_VM_HOST=20.24.50.121
HEICODE_VM_USER=heicode
HEICODE_VM_PASSWORD="<FILL_MANUALLY: 从负责人安全渠道获取,不要提交>"
HEICODE_VM_SSH_KEY_PATH="<OPTIONAL: 如果使用私钥登录,填写本机私钥路径>"
### 2. Gitee 仓库账号
HEICODE_GIT_REMOTE="http://gitee.ath.cx:3000/taijibaga/heicode-mananger.git"
GITEE_USERNAME="<FILL_MANUALLY: Gitee 用户名>"
GITEE_PASSWORD_OR_TOKEN="<FILL_MANUALLY: Gitee 密码或个人 token>"
### 3. Manager 生产数据库和缓存
# 获取位置:
# - VM: /home/heicode/apps/heicode-git/heicode/.env
# - 或 docker inspect heicode 的 Config.Env
# - 或 Azure PostgreSQL / Azure Redis 控制台
SQL_DSN="<FILL_FROM_VM_ENV_OR_AZURE_POSTGRESQL>"
REDIS_CONN_STRING="<FILL_FROM_VM_ENV_OR_AZURE_REDIS>"
### 4. Manager 基础安全密钥
# 获取位置:
# - VM .env
# - docker inspect heicode
SESSION_SECRET="<FILL_FROM_VM_ENV>"
CRYPTO_SECRET="<FILL_FROM_VM_ENV>"
MCP_SERVER_INTERNAL_TOKEN="<FILL_FROM_VM_ENV>"
### 5. Heicode / Agent 认证网关
HEICODE_AUTH_BASE_URL="<FILL_FROM_VM_ENV_OR_DEFAULT:https://apimtaiji.azure-api.net/api/mcp>"
HEICODE_ROOT_EMAILS="xiaohei@qq.com"
HEICODE_ADMIN_EMAILS="<OPTIONAL>"
### 6. Azure Key Vault
AZURE_KEY_VAULT_URL="https://heicode-kv.vault.azure.net"
AZURE_CLIENT_ID="<FILL_IF_USER_ASSIGNED_IDENTITY_IS_USED>"
AZURE_PORTAL_ACCOUNT="<FILL_MANUALLY: Azure 登录账号,不建议写密码>"
AZURE_PORTAL_PASSWORD="<DO_NOT_STORE_HERE_UNLESS_TEMPORARY_AND_LOCAL_ONLY>"
AZURE_KEY_VAULT_CALLBACK_SECRET_NAME="agent-callback-signing-key"
AZURE_KEY_VAULT_CALLBACK_SECRET_VALUE="<FILL_FROM_KEY_VAULT_IF_NEEDED>"
### 7. 普通 sub Agent Manager Runtime
AGENT_RUNTIME_ENABLED=true
AGENT_RUNTIME_BASE_URL="http://20.212.121.126"
AGENT_RUNTIME_HEALTH_PATH="/api/agent/health"
AGENT_RUNTIME_CREATE_PATH="/api/swarms"
AGENT_RUNTIME_STOP_PATH="/api/swarms/{swarm_id}/stop"
AGENT_RUNTIME_APPROVAL_DECISION_PATH="/api/swarms/{swarm_id}/approvals/{approval_id}"
AGENT_RUNTIME_TIMEOUT_SECONDS=15
AGENT_RUNTIME_SERVICE_TOKEN="<FILL_FROM_VM_ENV_OR_AGENT_MANAGER_OWNER>"
AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF="<FILL_FROM_VM_ENV_OR_KEY_VAULT_SECRET_REF>"
AGENT_CALLBACK_TOKEN="<FILL_FROM_VM_ENV_OR_AGENT_MANAGER_OWNER>"
AGENT_CALLBACK_SIGNING_SECRET="<FILL_FROM_VM_ENV_OR_KEY_VAULT>"
### 8. 蜂群 Runtime / HeiCode-Swarm
SWARM_RUNTIME_ENABLED=false
SWARM_RUNTIME_BASE_URL="http://52.139.240.116:8000"
SWARM_RUNTIME_CREATE_PATH="/api/swarms"
SWARM_RUNTIME_HEALTH_PATH="/api/agent/health"
SWARM_RUNTIME_STOP_PATH="/api/swarms/{swarm_id}/stop"
SWARM_RUNTIME_APPROVAL_DECISION_PATH="/api/swarms/{swarm_id}/approvals/{approval_id}"
SWARM_RUNTIME_SERVICE_TOKEN="<FILL_FROM_SWARM_OWNER_IF_ENABLED>"
### 9. NewAPI
NEWAPI_CONTAINER_NAME="new-api"
NEWAPI_VM_URL="http://20.24.50.121:3001"
NEWAPI_COMPOSE_PATH="/home/heicode/apps/new-api/docker-compose.yml"
NEWAPI_ADMIN_USERNAME="<FILL_MANUALLY: NewAPI 管理员账号>"
NEWAPI_ADMIN_PASSWORD="<FILL_MANUALLY: NewAPI 管理员密码>"
NEWAPI_CHANNEL_KEYS="<DO_NOT_STORE_MULTILINE_KEYS_HERE; 从 NewAPI 后台渠道管理读取>"
### 10. Manager 测试/管理员用户
MANAGER_PUBLIC_URL="https://code.xinghanlab.com"
MANAGER_ROOT_EMAIL="xiaohei@qq.com"
MANAGER_TEST_EMAIL="zsbgnw@gmail.com"
MANAGER_TEST_PASSWORD="<FILL_MANUALLY: 测试用户密码>"
MANAGER_ADMIN_EMAIL="<FILL_MANUALLY>"
MANAGER_ADMIN_PASSWORD="<FILL_MANUALLY>"
### 11. 桌面客户端安装包
HEICODE_DESKTOP_ARTIFACT_DIR="/data/desktop-artifacts"
HEICODE_DESKTOP_CLIENT_VERSION="0.1.0"
HEICODE_DESKTOP_FILE_MACOS_ARM64="/data/desktop-artifacts/HeiCode_0.1.0_macos_arm64.dmg"
HEICODE_DESKTOP_FILE_WINDOWS="/data/desktop-artifacts/Heicode_0.1.0_windows_x64_msi.msi"
@@ -13,20 +13,20 @@ import (
)
const (
agnetApprovalStatusPending = "pending"
agnetApprovalStatusApproved = "approved"
agnetApprovalStatusRejected = "rejected"
agnetApprovalStatusExpired = "expired"
agentApprovalStatusPending = "pending"
agentApprovalStatusApproved = "approved"
agentApprovalStatusRejected = "rejected"
agentApprovalStatusExpired = "expired"
agnetLeaseStatusActive = "active"
agnetLeaseStatusExpired = "expired"
agnetLeaseStatusRevoked = "revoked"
agentLeaseStatusActive = "active"
agentLeaseStatusExpired = "expired"
agentLeaseStatusRevoked = "revoked"
defaultAgnetApprovalTTLSeconds = 15 * 60
maxAgnetApprovalTTLSeconds = 60 * 60
defaultAgentApprovalTTLSeconds = 15 * 60
maxAgentApprovalTTLSeconds = 60 * 60
)
type agnetApprovalPayload struct {
type agentApprovalPayload struct {
DeploymentID string `json:"deployment_id"`
BindingScope string `json:"binding_scope"`
Operation string `json:"operation"`
@@ -41,11 +41,11 @@ type agnetApprovalPayload struct {
Reason string `json:"reason"`
}
type agnetDecisionPayload struct {
type agentDecisionPayload struct {
Reason string `json:"reason"`
}
type agnetApprovalResponse struct {
type agentApprovalResponse struct {
ApprovalID string `json:"approval_id"`
UserId int `json:"user_id"`
DeploymentID string `json:"deployment_id"`
@@ -68,10 +68,10 @@ type agnetApprovalResponse struct {
DecidedAt int64 `json:"decided_at,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
CredentialLease *agnetCredentialLeaseResponse `json:"credential_lease,omitempty"`
CredentialLease *agentCredentialLeaseResponse `json:"credential_lease,omitempty"`
}
type agnetCredentialLeaseResponse struct {
type agentCredentialLeaseResponse struct {
LeaseID string `json:"lease_id"`
CredentialRef string `json:"credential_ref"`
ApprovalID string `json:"approval_id"`
@@ -90,13 +90,13 @@ type agnetCredentialLeaseResponse struct {
UpdatedAt int64 `json:"updated_at"`
}
func CreateAgnetApprovalRequest(c *gin.Context) {
var payload agnetApprovalPayload
func CreateAgentApprovalRequest(c *gin.Context) {
var payload agentApprovalPayload
if err := common.DecodeJson(c.Request.Body, &payload); err != nil {
common.ApiError(c, err)
return
}
payload, err := normalizeAgnetApprovalPayload(payload)
payload, err := normalizeAgentApprovalPayload(payload)
if err != nil {
common.ApiError(c, err)
return
@@ -109,7 +109,7 @@ func CreateAgnetApprovalRequest(c *gin.Context) {
}
now := time.Now().UnixMilli()
approval := model.AgnetApprovalRequest{
approval := model.AgentApprovalRequest{
ApprovalID: "appr_" + common.GetUUID(),
UserId: userID,
DeploymentID: payload.DeploymentID,
@@ -122,8 +122,8 @@ func CreateAgnetApprovalRequest(c *gin.Context) {
RiskLevel: payload.RiskLevel,
RequiresCredential: payload.RequiresCredential,
SecretRef: payload.SecretRef,
Status: agnetApprovalStatusPending,
RequestedBy: agnetActorForUser(userID),
Status: agentApprovalStatusPending,
RequestedBy: agentActorForUser(userID),
RequestReason: payload.Reason,
TTLSeconds: payload.TTLSeconds,
ExpiresAt: now + int64(payload.TTLSeconds)*1000,
@@ -132,11 +132,11 @@ func CreateAgnetApprovalRequest(c *gin.Context) {
common.ApiError(c, err)
return
}
recordAgnetApprovalAudit("approval.requested", &approval, nil, "ok", "")
common.ApiSuccess(c, agnetApprovalToResponse(approval, nil))
recordAgentApprovalAudit("approval.requested", &approval, nil, "ok", "")
common.ApiSuccess(c, agentApprovalToResponse(approval, nil))
}
func ListAgnetApprovalRequests(c *gin.Context) {
func ListAgentApprovalRequests(c *gin.Context) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
@@ -151,58 +151,58 @@ func ListAgnetApprovalRequests(c *gin.Context) {
q = q.Where("deployment_id = ?", deploymentID)
}
var approvals []model.AgnetApprovalRequest
var approvals []model.AgentApprovalRequest
if err := q.Order("created_at desc, id desc").Limit(200).Find(&approvals).Error; err != nil {
common.ApiError(c, err)
return
}
for i := range approvals {
expireAgnetApprovalIfNeeded(&approvals[i])
expireAgentApprovalIfNeeded(&approvals[i])
}
items := make([]agnetApprovalResponse, 0, len(approvals))
items := make([]agentApprovalResponse, 0, len(approvals))
for _, approval := range approvals {
if statusFilter != "" && approval.Status != statusFilter {
continue
}
items = append(items, agnetApprovalToResponse(approval, nil))
items = append(items, agentApprovalToResponse(approval, nil))
}
common.ApiSuccess(c, gin.H{"items": items})
}
func GetAgnetApprovalRequest(c *gin.Context) {
approval, ok := findAgnetApprovalForUser(c)
func GetAgentApprovalRequest(c *gin.Context) {
approval, ok := findAgentApprovalForUser(c)
if !ok {
return
}
expireAgnetApprovalIfNeeded(&approval)
lease := findAgnetCredentialLeaseByApproval(approval.ApprovalID)
common.ApiSuccess(c, agnetApprovalToResponse(approval, lease))
expireAgentApprovalIfNeeded(&approval)
lease := findAgentCredentialLeaseByApproval(approval.ApprovalID)
common.ApiSuccess(c, agentApprovalToResponse(approval, lease))
}
func ApproveAgnetApprovalRequest(c *gin.Context) {
approval, ok := findAgnetApprovalForUser(c)
func ApproveAgentApprovalRequest(c *gin.Context) {
approval, ok := findAgentApprovalForUser(c)
if !ok {
return
}
if expireAgnetApprovalIfNeeded(&approval) {
if expireAgentApprovalIfNeeded(&approval) {
common.ApiErrorMsg(c, "approval request expired")
return
}
if approval.Status != agnetApprovalStatusPending {
if approval.Status != agentApprovalStatusPending {
common.ApiErrorMsg(c, "approval request is not pending")
return
}
var payload agnetDecisionPayload
var payload agentDecisionPayload
_ = common.DecodeJson(c.Request.Body, &payload)
now := time.Now().UnixMilli()
approval.Status = agnetApprovalStatusApproved
approval.DecidedBy = agnetActorForUser(c.GetInt("id"))
approval.Status = agentApprovalStatusApproved
approval.DecidedBy = agentActorForUser(c.GetInt("id"))
approval.DecisionReason = strings.TrimSpace(payload.Reason)
approval.DecidedAt = now
var lease *model.AgnetCredentialLease
var lease *model.AgentCredentialLease
err := model.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&approval).Error; err != nil {
return err
@@ -210,7 +210,7 @@ func ApproveAgnetApprovalRequest(c *gin.Context) {
if !approval.RequiresCredential {
return nil
}
createdLease := model.AgnetCredentialLease{
createdLease := model.AgentCredentialLease{
LeaseID: "lease_" + common.GetUUID(),
ApprovalID: approval.ApprovalID,
UserId: approval.UserId,
@@ -221,11 +221,11 @@ func ApproveAgnetApprovalRequest(c *gin.Context) {
ResourceScope: approval.ResourceScope,
TargetRole: approval.TargetRole,
SecretRef: approval.SecretRef,
Status: agnetLeaseStatusActive,
Status: agentLeaseStatusActive,
TTLSeconds: approval.TTLSeconds,
ExpiresAt: now + int64(approval.TTLSeconds)*1000,
}
createdLease.CredentialRef = "lease://agnet/" + createdLease.LeaseID
createdLease.CredentialRef = "lease://agent/" + createdLease.LeaseID
if err := tx.Create(&createdLease).Error; err != nil {
return err
}
@@ -240,44 +240,44 @@ func ApproveAgnetApprovalRequest(c *gin.Context) {
common.ApiError(c, err)
return
}
recordAgnetApprovalAudit("approval.approved", &approval, nil, "ok", "")
recordAgentApprovalAudit("approval.approved", &approval, nil, "ok", "")
if lease != nil {
recordAgnetApprovalAudit("credential_lease.created", &approval, lease, "ok", "")
recordAgentApprovalAudit("credential_lease.created", &approval, lease, "ok", "")
}
syncAgnetRuntimeApprovalDecision(c, &approval, lease, agnetApprovalStatusApproved)
common.ApiSuccess(c, agnetApprovalToResponse(approval, lease))
syncAgentRuntimeApprovalDecision(c, &approval, lease, agentApprovalStatusApproved)
common.ApiSuccess(c, agentApprovalToResponse(approval, lease))
}
func RejectAgnetApprovalRequest(c *gin.Context) {
approval, ok := findAgnetApprovalForUser(c)
func RejectAgentApprovalRequest(c *gin.Context) {
approval, ok := findAgentApprovalForUser(c)
if !ok {
return
}
if expireAgnetApprovalIfNeeded(&approval) {
if expireAgentApprovalIfNeeded(&approval) {
common.ApiErrorMsg(c, "approval request expired")
return
}
if approval.Status != agnetApprovalStatusPending {
if approval.Status != agentApprovalStatusPending {
common.ApiErrorMsg(c, "approval request is not pending")
return
}
var payload agnetDecisionPayload
var payload agentDecisionPayload
_ = common.DecodeJson(c.Request.Body, &payload)
approval.Status = agnetApprovalStatusRejected
approval.DecidedBy = agnetActorForUser(c.GetInt("id"))
approval.Status = agentApprovalStatusRejected
approval.DecidedBy = agentActorForUser(c.GetInt("id"))
approval.DecisionReason = strings.TrimSpace(payload.Reason)
approval.DecidedAt = time.Now().UnixMilli()
if err := model.DB.Save(&approval).Error; err != nil {
common.ApiError(c, err)
return
}
recordAgnetApprovalAudit("approval.rejected", &approval, nil, "ok", "")
syncAgnetRuntimeApprovalDecision(c, &approval, nil, agnetApprovalStatusRejected)
common.ApiSuccess(c, agnetApprovalToResponse(approval, nil))
recordAgentApprovalAudit("approval.rejected", &approval, nil, "ok", "")
syncAgentRuntimeApprovalDecision(c, &approval, nil, agentApprovalStatusRejected)
common.ApiSuccess(c, agentApprovalToResponse(approval, nil))
}
func ListAgnetCredentialLeases(c *gin.Context) {
func ListAgentCredentialLeases(c *gin.Context) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
@@ -292,23 +292,23 @@ func ListAgnetCredentialLeases(c *gin.Context) {
q = q.Where("deployment_id = ?", deploymentID)
}
var leases []model.AgnetCredentialLease
var leases []model.AgentCredentialLease
if err := q.Order("created_at desc, id desc").Limit(200).Find(&leases).Error; err != nil {
common.ApiError(c, err)
return
}
items := make([]agnetCredentialLeaseResponse, 0, len(leases))
items := make([]agentCredentialLeaseResponse, 0, len(leases))
for i := range leases {
expireAgnetCredentialLeaseIfNeeded(&leases[i])
expireAgentCredentialLeaseIfNeeded(&leases[i])
if statusFilter != "" && leases[i].Status != statusFilter {
continue
}
items = append(items, agnetLeaseToResponse(leases[i]))
items = append(items, agentLeaseToResponse(leases[i]))
}
common.ApiSuccess(c, gin.H{"items": items})
}
func RevokeAgnetCredentialLease(c *gin.Context) {
func RevokeAgentCredentialLease(c *gin.Context) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
@@ -319,7 +319,7 @@ func RevokeAgnetCredentialLease(c *gin.Context) {
common.ApiErrorMsg(c, "lease_id required")
return
}
var lease model.AgnetCredentialLease
var lease model.AgentCredentialLease
if err := model.DB.Where("lease_id = ? AND user_id = ?", leaseID, userID).First(&lease).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "credential lease not found")
@@ -328,28 +328,28 @@ func RevokeAgnetCredentialLease(c *gin.Context) {
common.ApiError(c, err)
return
}
if expireAgnetCredentialLeaseIfNeeded(&lease) {
common.ApiSuccess(c, agnetLeaseToResponse(lease))
if expireAgentCredentialLeaseIfNeeded(&lease) {
common.ApiSuccess(c, agentLeaseToResponse(lease))
return
}
if lease.Status != agnetLeaseStatusActive {
if lease.Status != agentLeaseStatusActive {
common.ApiErrorMsg(c, "credential lease is not active")
return
}
lease.Status = agnetLeaseStatusRevoked
lease.Status = agentLeaseStatusRevoked
lease.RevokedAt = time.Now().UnixMilli()
if err := model.DB.Save(&lease).Error; err != nil {
common.ApiError(c, err)
return
}
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ?", lease.ApprovalID).First(&approval).Error; err == nil {
recordAgnetApprovalAudit("credential_lease.revoked", &approval, &lease, "ok", "")
recordAgentApprovalAudit("credential_lease.revoked", &approval, &lease, "ok", "")
}
common.ApiSuccess(c, agnetLeaseToResponse(lease))
common.ApiSuccess(c, agentLeaseToResponse(lease))
}
func normalizeAgnetApprovalPayload(p agnetApprovalPayload) (agnetApprovalPayload, error) {
func normalizeAgentApprovalPayload(p agentApprovalPayload) (agentApprovalPayload, error) {
p.DeploymentID = strings.TrimSpace(p.DeploymentID)
p.BindingScope = strings.TrimSpace(p.BindingScope)
p.Operation = strings.TrimSpace(p.Operation)
@@ -380,10 +380,10 @@ func normalizeAgnetApprovalPayload(p agnetApprovalPayload) (agnetApprovalPayload
return p, errors.New("risk_level must be low, medium, high, or critical")
}
if p.TTLSeconds <= 0 {
p.TTLSeconds = defaultAgnetApprovalTTLSeconds
p.TTLSeconds = defaultAgentApprovalTTLSeconds
}
if p.TTLSeconds > maxAgnetApprovalTTLSeconds {
p.TTLSeconds = maxAgnetApprovalTTLSeconds
if p.TTLSeconds > maxAgentApprovalTTLSeconds {
p.TTLSeconds = maxAgentApprovalTTLSeconds
}
if p.RequiresCredential {
if p.SecretRef == "" {
@@ -396,73 +396,73 @@ func normalizeAgnetApprovalPayload(p agnetApprovalPayload) (agnetApprovalPayload
return p, nil
}
func findAgnetApprovalForUser(c *gin.Context) (model.AgnetApprovalRequest, bool) {
func findAgentApprovalForUser(c *gin.Context) (model.AgentApprovalRequest, bool) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
approvalID := strings.TrimSpace(c.Param("approval_id"))
if approvalID == "" {
common.ApiErrorMsg(c, "approval_id required")
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ? AND user_id = ?", approvalID, userID).First(&approval).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "approval request not found")
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
common.ApiError(c, err)
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
return approval, true
}
func findAgnetCredentialLeaseByApproval(approvalID string) *model.AgnetCredentialLease {
var lease model.AgnetCredentialLease
func findAgentCredentialLeaseByApproval(approvalID string) *model.AgentCredentialLease {
var lease model.AgentCredentialLease
if err := model.DB.Where("approval_id = ?", approvalID).First(&lease).Error; err != nil {
return nil
}
expireAgnetCredentialLeaseIfNeeded(&lease)
expireAgentCredentialLeaseIfNeeded(&lease)
return &lease
}
func expireAgnetApprovalIfNeeded(approval *model.AgnetApprovalRequest) bool {
if approval == nil || approval.Status != agnetApprovalStatusPending {
func expireAgentApprovalIfNeeded(approval *model.AgentApprovalRequest) bool {
if approval == nil || approval.Status != agentApprovalStatusPending {
return false
}
if approval.ExpiresAt <= 0 || approval.ExpiresAt > time.Now().UnixMilli() {
return false
}
approval.Status = agnetApprovalStatusExpired
approval.Status = agentApprovalStatusExpired
approval.DecidedAt = time.Now().UnixMilli()
if err := model.DB.Save(approval).Error; err == nil {
recordAgnetApprovalAudit("approval.expired", approval, nil, "ok", "")
recordAgentApprovalAudit("approval.expired", approval, nil, "ok", "")
}
return true
}
func expireAgnetCredentialLeaseIfNeeded(lease *model.AgnetCredentialLease) bool {
if lease == nil || lease.Status != agnetLeaseStatusActive {
func expireAgentCredentialLeaseIfNeeded(lease *model.AgentCredentialLease) bool {
if lease == nil || lease.Status != agentLeaseStatusActive {
return false
}
if lease.ExpiresAt <= 0 || lease.ExpiresAt > time.Now().UnixMilli() {
return false
}
lease.Status = agnetLeaseStatusExpired
lease.Status = agentLeaseStatusExpired
if err := model.DB.Save(lease).Error; err != nil {
return false
}
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ?", lease.ApprovalID).First(&approval).Error; err == nil {
recordAgnetApprovalAudit("credential_lease.expired", &approval, lease, "ok", "")
recordAgentApprovalAudit("credential_lease.expired", &approval, lease, "ok", "")
}
return true
}
func agnetApprovalToResponse(approval model.AgnetApprovalRequest, lease *model.AgnetCredentialLease) agnetApprovalResponse {
resp := agnetApprovalResponse{
func agentApprovalToResponse(approval model.AgentApprovalRequest, lease *model.AgentCredentialLease) agentApprovalResponse {
resp := agentApprovalResponse{
ApprovalID: approval.ApprovalID,
UserId: approval.UserId,
DeploymentID: approval.DeploymentID,
@@ -487,14 +487,14 @@ func agnetApprovalToResponse(approval model.AgnetApprovalRequest, lease *model.A
UpdatedAt: approval.UpdatedAt,
}
if lease != nil {
leaseResp := agnetLeaseToResponse(*lease)
leaseResp := agentLeaseToResponse(*lease)
resp.CredentialLease = &leaseResp
}
return resp
}
func agnetLeaseToResponse(lease model.AgnetCredentialLease) agnetCredentialLeaseResponse {
return agnetCredentialLeaseResponse{
func agentLeaseToResponse(lease model.AgentCredentialLease) agentCredentialLeaseResponse {
return agentCredentialLeaseResponse{
LeaseID: lease.LeaseID,
CredentialRef: lease.CredentialRef,
ApprovalID: lease.ApprovalID,
@@ -514,7 +514,7 @@ func agnetLeaseToResponse(lease model.AgnetCredentialLease) agnetCredentialLease
}
}
func recordAgnetApprovalAudit(event string, approval *model.AgnetApprovalRequest, lease *model.AgnetCredentialLease, result string, message string) {
func recordAgentApprovalAudit(event string, approval *model.AgentApprovalRequest, lease *model.AgentCredentialLease, result string, message string) {
if approval == nil {
return
}
@@ -540,7 +540,7 @@ func recordAgnetApprovalAudit(event string, approval *model.AgnetApprovalRequest
if raw, err := common.Marshal(details); err == nil {
detailsJSON = string(raw)
}
model.InsertAgnetAuditEvent(&model.AgnetAuditEvent{
model.InsertAgentAuditEvent(&model.AgentAuditEvent{
EventID: "evt_" + common.GetUUID(),
Event: event,
Actor: "manager",
@@ -555,6 +555,6 @@ func recordAgnetApprovalAudit(event string, approval *model.AgnetApprovalRequest
})
}
func agnetActorForUser(userID int) string {
func agentActorForUser(userID int) string {
return fmt.Sprintf("user:%d", userID)
}
@@ -17,7 +17,7 @@ import (
"gorm.io/gorm"
)
func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB {
func setupAgentApprovalTestDB(t *testing.T) *gorm.DB {
t.Helper()
gin.SetMode(gin.TestMode)
common.UsingSQLite = true
@@ -31,10 +31,10 @@ func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB {
model.DB = db
model.LOG_DB = db
require.NoError(t, db.AutoMigrate(
&model.AgnetApprovalRequest{},
&model.AgnetCredentialLease{},
&model.AgnetAuditEvent{},
&model.AgnetDeployment{},
&model.AgentApprovalRequest{},
&model.AgentCredentialLease{},
&model.AgentAuditEvent{},
&model.AgentDeployment{},
))
t.Cleanup(func() {
sqlDB, err := db.DB()
@@ -45,8 +45,8 @@ func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB {
return db
}
func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
db := setupAgentApprovalTestDB(t)
var runtimeBody string
var runtimeAuth string
var runtimePath string
@@ -59,12 +59,12 @@ func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
_, _ = 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}")
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
t.Setenv("AGENT_RUNTIME_BASE_URL", runtimeServer.URL)
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "runtime-token")
t.Setenv("AGENT_RUNTIME_APPROVAL_DECISION_PATH", "/api/swarms/{swarm_id}/approvals/{approval_id}")
require.NoError(t, db.Create(&model.AgnetDeployment{
require.NoError(t, db.Create(&model.AgentDeployment{
DeploymentID: "dep_runtime_approval",
UserID: "7",
BindingScope: "project-alpha",
@@ -90,8 +90,8 @@ func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
"ttl_seconds":600
}`
_, approvalID := createAgnetApprovalForTest(t, 7, body)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许写入"}`)
_, approvalID := createAgentApprovalForTest(t, 7, body)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 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)
@@ -100,16 +100,16 @@ func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
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.Contains(t, runtimeBody, `"credential_ref":"lease://agent/`)
require.NotContains(t, runtimeBody, "azkv://")
var auditRows []model.AgnetAuditEvent
var auditRows []model.AgentAuditEvent
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 {
func performAgentApprovalRequest(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) {
c.Set("id", userID)
@@ -122,18 +122,18 @@ func performAgnetApprovalRequest(handler gin.HandlerFunc, userID int, method str
return w
}
func decodeAgnetApprovalEnvelope(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
func decodeAgentApprovalEnvelope(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
t.Helper()
var envelope map[string]any
require.NoError(t, common.Unmarshal(w.Body.Bytes(), &envelope))
return envelope
}
func createAgnetApprovalForTest(t *testing.T, userID int, body string) (map[string]any, string) {
func createAgentApprovalForTest(t *testing.T, userID int, body string) (map[string]any, string) {
t.Helper()
w := performAgnetApprovalRequest(CreateAgnetApprovalRequest, userID, http.MethodPost, "/approvals", "/approvals", body)
w := performAgentApprovalRequest(CreateAgentApprovalRequest, userID, http.MethodPost, "/approvals", "/approvals", body)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
approvalID := data["approval_id"].(string)
@@ -141,8 +141,8 @@ func createAgnetApprovalForTest(t *testing.T, userID int, body string) (map[stri
return data, approvalID
}
func TestAgnetApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_1",
"binding_scope":"project-alpha",
@@ -158,27 +158,27 @@ func TestAgnetApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
"reason":"需要写入功能分支"
}`
_, approvalID := createAgnetApprovalForTest(t, 7, body)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许本次任务"}`)
_, approvalID := createAgentApprovalForTest(t, 7, body)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许本次任务"}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
require.Equal(t, "approved", data["status"])
lease := data["credential_lease"].(map[string]any)
require.Equal(t, "active", lease["status"])
require.Contains(t, lease["credential_ref"], "lease://agnet/")
require.Contains(t, lease["credential_ref"], "lease://agent/")
require.NotContains(t, w.Body.String(), "do-not-store")
require.NotContains(t, w.Body.String(), "azkv://", "API responses must not expose internal Key Vault secret_ref")
var storedLease model.AgnetCredentialLease
var storedLease model.AgentCredentialLease
require.NoError(t, db.Where("approval_id = ?", approvalID).First(&storedLease).Error)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo-main", storedLease.SecretRef)
require.Equal(t, "active", storedLease.Status)
require.Greater(t, storedLease.ExpiresAt, storedLease.CreatedAt)
require.NotContains(t, storedLease.CredentialRef, "azkv://")
var auditRows []model.AgnetAuditEvent
var auditRows []model.AgentAuditEvent
require.NoError(t, db.Order("id asc").Find(&auditRows).Error)
require.Len(t, auditRows, 3)
require.Equal(t, "approval.requested", auditRows[0].Event)
@@ -187,8 +187,8 @@ func TestAgnetApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
require.NotContains(t, auditRows[2].DetailsJSON, "repo-main-secret-value")
}
func TestAgnetApprovalRejectDoesNotCreateLease(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalRejectDoesNotCreateLease(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_2",
"binding_scope":"project-alpha",
@@ -203,21 +203,21 @@ func TestAgnetApprovalRejectDoesNotCreateLease(t *testing.T) {
"ttl_seconds":300
}`
_, approvalID := createAgnetApprovalForTest(t, 8, body)
w := performAgnetApprovalRequest(RejectAgnetApprovalRequest, 8, http.MethodPost, "/approvals/:approval_id/reject", "/approvals/"+approvalID+"/reject", `{"reason":"风险过高"}`)
_, approvalID := createAgentApprovalForTest(t, 8, body)
w := performAgentApprovalRequest(RejectAgentApprovalRequest, 8, http.MethodPost, "/approvals/:approval_id/reject", "/approvals/"+approvalID+"/reject", `{"reason":"风险过高"}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
require.Equal(t, "rejected", data["status"])
var leases int64
require.NoError(t, db.Model(&model.AgnetCredentialLease{}).Count(&leases).Error)
require.NoError(t, db.Model(&model.AgentCredentialLease{}).Count(&leases).Error)
require.Equal(t, int64(0), leases)
}
func TestAgnetApprovalExpiredRequestCannotBeApproved(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalExpiredRequestCannotBeApproved(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_3",
"binding_scope":"project-alpha",
@@ -232,24 +232,24 @@ func TestAgnetApprovalExpiredRequestCannotBeApproved(t *testing.T) {
"ttl_seconds":1
}`
_, approvalID := createAgnetApprovalForTest(t, 9, body)
require.NoError(t, db.Model(&model.AgnetApprovalRequest{}).Where("approval_id = ?", approvalID).Updates(map[string]any{
_, approvalID := createAgentApprovalForTest(t, 9, body)
require.NoError(t, db.Model(&model.AgentApprovalRequest{}).Where("approval_id = ?", approvalID).Updates(map[string]any{
"expires_at": 1,
}).Error)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 9, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 9, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, false, envelope["success"])
require.Contains(t, envelope["message"], "approval request expired")
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
require.NoError(t, db.Where("approval_id = ?", approvalID).First(&approval).Error)
require.Equal(t, "expired", approval.Status)
}
func TestAgnetCredentialLeaseCanBeRevoked(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentCredentialLeaseCanBeRevoked(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_4",
"binding_scope":"project-alpha",
@@ -264,16 +264,16 @@ func TestAgnetCredentialLeaseCanBeRevoked(t *testing.T) {
"ttl_seconds":600
}`
_, approvalID := createAgnetApprovalForTest(t, 10, body)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 10, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
_, approvalID := createAgentApprovalForTest(t, 10, body)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 10, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
require.Equal(t, http.StatusOK, w.Code)
var storedLease model.AgnetCredentialLease
var storedLease model.AgentCredentialLease
require.NoError(t, db.Where("approval_id = ?", approvalID).First(&storedLease).Error)
revokePath := "/credential-leases/" + storedLease.LeaseID + "/revoke"
w = performAgnetApprovalRequest(RevokeAgnetCredentialLease, 10, http.MethodPost, "/credential-leases/:lease_id/revoke", revokePath, `{"reason":"任务结束"}`)
w = performAgentApprovalRequest(RevokeAgentCredentialLease, 10, http.MethodPost, "/credential-leases/:lease_id/revoke", revokePath, `{"reason":"任务结束"}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
require.Equal(t, "revoked", data["status"])
@@ -19,7 +19,7 @@ import (
"github.com/heicode/manager/model"
)
type agnetCallbackEnvelope struct {
type agentCallbackEnvelope struct {
EventID string `json:"event_id"`
IdempotencyKey string `json:"idempotency_key"`
EventType string `json:"event_type"`
@@ -32,10 +32,10 @@ type agnetCallbackEnvelope struct {
Source string `json:"source"`
Metadata map[string]any `json:"metadata"`
Payload map[string]any `json:"payload"`
Artifact agnetArtifactPayload `json:"artifact"`
Artifact agentArtifactPayload `json:"artifact"`
}
type agnetArtifactPayload struct {
type agentArtifactPayload struct {
ArtifactID string `json:"artifact_id"`
ArtifactType string `json:"artifact_type"`
Title string `json:"title"`
@@ -45,8 +45,8 @@ type agnetArtifactPayload struct {
Metadata map[string]any `json:"metadata"`
}
func agnetCallbackTokenFromRequest(c *gin.Context) string {
if token := strings.TrimSpace(c.GetHeader("X-Agnet-Service-Token")); token != "" {
func agentCallbackTokenFromRequest(c *gin.Context) string {
if token := strings.TrimSpace(c.GetHeader("X-Agent-Service-Token")); token != "" {
return token
}
auth := strings.TrimSpace(c.GetHeader("Authorization"))
@@ -56,25 +56,25 @@ func agnetCallbackTokenFromRequest(c *gin.Context) string {
return ""
}
func agnetCallbackSigningSecret() string {
if secret := strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_CALLBACK_SIGNING_SECRET", "")); secret != "" {
func agentCallbackSigningSecret() string {
if secret := strings.TrimSpace(common.GetEnvOrDefaultString("AGENT_CALLBACK_SIGNING_SECRET", "")); secret != "" {
return secret
}
secretRef := firstNonEmpty(
common.GetEnvOrDefaultString("AGNET_CALLBACK_SIGNING_SECRET_REF", ""),
common.GetEnvOrDefaultString("AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""),
common.GetEnvOrDefaultString("AGENT_CALLBACK_SIGNING_SECRET_REF", ""),
common.GetEnvOrDefaultString("AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""),
)
if secretRef == "" {
return ""
}
client, err := newSecretStoreClientFromEnv()
if err != nil {
common.SysLog("agnetCallbackSigningSecret: " + err.Error())
common.SysLog("agentCallbackSigningSecret: " + err.Error())
return ""
}
data, err := client.getJSONSecret(secretRef)
if err != nil {
common.SysLog("agnetCallbackSigningSecret: " + err.Error())
common.SysLog("agentCallbackSigningSecret: " + err.Error())
return ""
}
for _, key := range []string{"callback_signing_secret", "signing_secret", "secret", "value"} {
@@ -82,38 +82,38 @@ func agnetCallbackSigningSecret() string {
return value
}
}
common.SysLog("agnetCallbackSigningSecret: signing secret is missing from Azure Key Vault payload")
common.SysLog("agentCallbackSigningSecret: signing secret is missing from Azure Key Vault payload")
return ""
}
func agnetCallbackSignatureTolerance() time.Duration {
seconds := common.GetEnvOrDefault("AGNET_CALLBACK_SIGNATURE_TOLERANCE_SECONDS", 300)
func agentCallbackSignatureTolerance() time.Duration {
seconds := common.GetEnvOrDefault("AGENT_CALLBACK_SIGNATURE_TOLERANCE_SECONDS", 300)
if seconds <= 0 {
seconds = 300
}
return time.Duration(seconds) * time.Second
}
func validateAgnetCallbackHMAC(c *gin.Context, rawBody []byte, eventID string) (bool, bool) {
secret := agnetCallbackSigningSecret()
func validateAgentCallbackHMAC(c *gin.Context, rawBody []byte, eventID string) (bool, bool) {
secret := agentCallbackSigningSecret()
if secret == "" {
return false, false
}
timestamp := strings.TrimSpace(c.GetHeader("X-Agnet-Timestamp"))
signature := strings.TrimSpace(c.GetHeader("X-Agnet-Signature"))
timestamp := strings.TrimSpace(c.GetHeader("X-Agent-Timestamp"))
signature := strings.TrimSpace(c.GetHeader("X-Agent-Signature"))
if timestamp == "" || signature == "" || eventID == "" {
return false, false
}
tsMs, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
agnetError(c, "CALLBACK_UNAUTHORIZED", "invalid callback timestamp")
agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback timestamp")
return true, false
}
now := time.Now()
eventTime := time.UnixMilli(tsMs)
tolerance := agnetCallbackSignatureTolerance()
tolerance := agentCallbackSignatureTolerance()
if eventTime.Before(now.Add(-tolerance)) || eventTime.After(now.Add(tolerance)) {
agnetError(c, "CALLBACK_UNAUTHORIZED", "callback timestamp outside allowed window")
agentError(c, "CALLBACK_UNAUTHORIZED", "callback timestamp outside allowed window")
return true, false
}
payload := timestamp + "." + eventID + "." + string(rawBody)
@@ -121,32 +121,32 @@ func validateAgnetCallbackHMAC(c *gin.Context, rawBody []byte, eventID string) (
mac.Write([]byte(payload))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
agnetError(c, "CALLBACK_UNAUTHORIZED", "invalid callback signature")
agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback signature")
return true, false
}
return true, true
}
func validateAgnetCallbackAuth(c *gin.Context, rawBody []byte, eventID string) bool {
if common.GetEnvOrDefaultBool("AGNET_CALLBACK_AUTH_DISABLED", false) {
func validateAgentCallbackAuth(c *gin.Context, rawBody []byte, eventID string) bool {
if common.GetEnvOrDefaultBool("AGENT_CALLBACK_AUTH_DISABLED", false) {
return true
}
if attempted, ok := validateAgnetCallbackHMAC(c, rawBody, eventID); attempted {
if attempted, ok := validateAgentCallbackHMAC(c, rawBody, eventID); attempted {
return ok
}
expected := strings.TrimSpace(os.Getenv("AGNET_CALLBACK_TOKEN"))
expected := strings.TrimSpace(os.Getenv("AGENT_CALLBACK_TOKEN"))
if expected == "" {
agnetError(c, "CALLBACK_UNAUTHORIZED", "callback token is not configured")
agentError(c, "CALLBACK_UNAUTHORIZED", "callback token is not configured")
return false
}
if agnetCallbackTokenFromRequest(c) != expected {
agnetError(c, "CALLBACK_UNAUTHORIZED", "invalid callback service token")
if agentCallbackTokenFromRequest(c) != expected {
agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback service token")
return false
}
return true
}
func agnetCallbackHasPlaintextSecret(payload agnetCallbackEnvelope) bool {
func agentCallbackHasPlaintextSecret(payload agentCallbackEnvelope) bool {
if containsPlaintextSecret(payload.Metadata) || containsPlaintextSecret(payload.Payload) || containsPlaintextSecret(payload.Artifact.Metadata) {
return true
}
@@ -158,43 +158,43 @@ func agnetCallbackHasPlaintextSecret(payload agnetCallbackEnvelope) bool {
return containsPlaintextSecret(asMap)
}
func agnetCallbackDeploymentContext(deploymentID string, swarmID string) (agnetDeploymentRecord, bool) {
func agentCallbackDeploymentContext(deploymentID string, swarmID string) (agentDeploymentRecord, bool) {
deploymentID = strings.TrimSpace(deploymentID)
swarmID = strings.TrimSpace(swarmID)
if deploymentID != "" {
if record, ok := findAgnetDeploymentRecord(deploymentID); ok {
if record, ok := findAgentDeploymentRecord(deploymentID); ok {
return record, true
}
}
runtimeID := firstNonEmpty(swarmID, deploymentID)
if runtimeID == "" {
return agnetDeploymentRecord{}, false
return agentDeploymentRecord{}, false
}
agnetMu.RLock()
for _, record := range agnetDeployments {
agentMu.RLock()
for _, record := range agentDeployments {
if strings.TrimSpace(record.RuntimeSwarmID) == runtimeID || strings.TrimSpace(record.RuntimeDeploymentID) == runtimeID {
agnetMu.RUnlock()
agentMu.RUnlock()
return record, true
}
}
agnetMu.RUnlock()
agentMu.RUnlock()
if model.DB == nil {
return agnetDeploymentRecord{}, false
return agentDeploymentRecord{}, false
}
var row model.AgnetDeployment
var row model.AgentDeployment
if err := model.DB.Where("runtime_swarm_id = ? OR runtime_deployment_id = ?", runtimeID, runtimeID).First(&row).Error; err != nil {
return agnetDeploymentRecord{}, false
return agentDeploymentRecord{}, false
}
record, err := agnetDeploymentModelToRecord(row)
record, err := agentDeploymentModelToRecord(row)
if err != nil {
common.SysLog("agnetCallbackDeploymentContext: " + err.Error())
return agnetDeploymentRecord{}, false
common.SysLog("agentCallbackDeploymentContext: " + err.Error())
return agentDeploymentRecord{}, false
}
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
return record, true
}
@@ -232,7 +232,7 @@ func callbackMapValue(values map[string]any, key string) map[string]any {
return nil
}
func agnetCallbackEventRuntimeState(eventType string, payload map[string]any) string {
func agentCallbackEventRuntimeState(eventType string, payload map[string]any) string {
if state := callbackStringValue(payload, "status"); state != "" {
return state
}
@@ -248,7 +248,7 @@ func agnetCallbackEventRuntimeState(eventType string, payload map[string]any) st
}
}
func upsertAgnetCallbackAgentInstance(record *agnetDeploymentRecord, payload agnetCallbackEnvelope, phase string, runtimeState string) bool {
func upsertAgentCallbackAgentInstance(record *agentDeploymentRecord, payload agentCallbackEnvelope, phase string, runtimeState string) bool {
if record == nil {
return false
}
@@ -284,7 +284,7 @@ func upsertAgnetCallbackAgentInstance(record *agnetDeploymentRecord, payload agn
return true
}
}
record.AgentInstances = append(record.AgentInstances, agnetAgentInstance{
record.AgentInstances = append(record.AgentInstances, agentAgentInstance{
InstanceID: firstNonEmpty(instanceID, "agi_"+common.GetUUID()[:12]),
Role: role,
Phase: firstNonEmpty(phase, record.Phase),
@@ -294,7 +294,7 @@ func upsertAgnetCallbackAgentInstance(record *agnetDeploymentRecord, payload agn
return true
}
func applyAgnetCallbackDeploymentState(payload agnetCallbackEnvelope, record agnetDeploymentRecord) (agnetDeploymentRecord, bool) {
func applyAgentCallbackDeploymentState(payload agentCallbackEnvelope, record agentDeploymentRecord) (agentDeploymentRecord, bool) {
if strings.TrimSpace(record.DeploymentID) == "" {
return record, false
}
@@ -342,25 +342,25 @@ func applyAgnetCallbackDeploymentState(payload agnetCallbackEnvelope, record agn
}
case "agent.started", "agent.completed", "agent.crashed":
phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase"), record.Phase)
runtimeState := agnetCallbackEventRuntimeState(payload.EventType, source)
if upsertAgnetCallbackAgentInstance(&record, payload, phase, runtimeState) {
runtimeState := agentCallbackEventRuntimeState(payload.EventType, source)
if upsertAgentCallbackAgentInstance(&record, payload, phase, runtimeState) {
changed = true
}
}
if !changed {
return record, false
}
record.UpdatedAt = firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agnetNow())
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
if err := updateAgnetDeploymentRecord(record); err != nil {
common.SysLog("applyAgnetCallbackDeploymentState: " + err.Error())
record.UpdatedAt = firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agentNow())
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
if err := updateAgentDeploymentRecord(record); err != nil {
common.SysLog("applyAgentCallbackDeploymentState: " + err.Error())
}
return record, true
}
func normalizeCallbackArtifact(payload *agnetCallbackEnvelope) {
func normalizeCallbackArtifact(payload *agentCallbackEnvelope) {
if payload == nil || strings.TrimSpace(payload.Artifact.ArtifactID) != "" {
return
}
@@ -371,7 +371,7 @@ func normalizeCallbackArtifact(payload *agnetCallbackEnvelope) {
if len(source) == 0 {
return
}
payload.Artifact = agnetArtifactPayload{
payload.Artifact = agentArtifactPayload{
ArtifactID: callbackStringValue(source, "artifact_id"),
ArtifactType: callbackStringValue(source, "artifact_type"),
Title: callbackStringValue(source, "title"),
@@ -399,7 +399,7 @@ func normalizeCallbackArtifact(payload *agnetCallbackEnvelope) {
}
}
func persistAgnetArtifactFromCallback(payload agnetCallbackEnvelope, record agnetDeploymentRecord) error {
func persistAgentArtifactFromCallback(payload agentCallbackEnvelope, record agentDeploymentRecord) error {
artifact := payload.Artifact
if strings.TrimSpace(artifact.ArtifactID) == "" {
return nil
@@ -410,7 +410,7 @@ func persistAgnetArtifactFromCallback(payload agnetCallbackEnvelope, record agne
metadataJSON = string(data)
}
}
return model.UpsertAgnetArtifact(&model.AgnetArtifact{
return model.UpsertAgentArtifact(&model.AgentArtifact{
ArtifactID: strings.TrimSpace(artifact.ArtifactID),
DeploymentID: strings.TrimSpace(payload.DeploymentID),
TaskID: strings.TrimSpace(payload.TaskID),
@@ -460,7 +460,7 @@ func callbackIntValue(values map[string]any, key string) int {
}
}
var agnetCallbackEventRequiredFields = map[string][]string{
var agentCallbackEventRequiredFields = map[string][]string{
"deployment.status_changed": {"status"},
"phase.changed": {"stage", "checkpoint"},
"agent.started": {"agent_role"},
@@ -486,7 +486,7 @@ var agnetCallbackEventRequiredFields = map[string][]string{
"budget.alert": {"threshold_pct"},
}
var agnetCallbackEventCategories = map[string]string{
var agentCallbackEventCategories = map[string]string{
"deployment.status_changed": "deployment",
"phase.changed": "ordinary_sub",
"agent.started": "ordinary_sub",
@@ -512,33 +512,33 @@ var agnetCallbackEventCategories = map[string]string{
"budget.alert": "budget",
}
func AgnetGetSwarmEventCallbackSchema(c *gin.Context) {
events := make([]string, 0, len(agnetCallbackEventRequiredFields))
for eventType := range agnetCallbackEventRequiredFields {
func AgentGetRuntimeEventCallbackSchema(c *gin.Context) {
events := make([]string, 0, len(agentCallbackEventRequiredFields))
for eventType := range agentCallbackEventRequiredFields {
events = append(events, eventType)
}
sort.Strings(events)
items := make([]gin.H, 0, len(events))
for _, eventType := range events {
required := append([]string(nil), agnetCallbackEventRequiredFields[eventType]...)
required := append([]string(nil), agentCallbackEventRequiredFields[eventType]...)
sort.Strings(required)
items = append(items, gin.H{
"event_type": eventType,
"category": agnetCallbackEventCategories[eventType],
"category": agentCallbackEventCategories[eventType],
"required_fields": required,
"payload_location": "top-level envelope or payload object; artifact_id may also be in artifact object",
})
}
common.ApiSuccess(c, gin.H{
"callback_path": "/api/agnet/callbacks/swarm-events",
"callback_path": "/api/agent/callbacks/runtime-events",
"auth": gin.H{
"service_token_headers": []string{"X-Agnet-Service-Token", "Authorization: Bearer <token>"},
"hmac_headers": []string{"X-Agnet-Event-Id", "X-Agnet-Timestamp", "X-Agnet-Signature"},
"service_token_headers": []string{"X-Agent-Service-Token", "Authorization: Bearer <token>"},
"hmac_headers": []string{"X-Agent-Event-Id", "X-Agent-Timestamp", "X-Agent-Signature"},
"hmac_payload": "timestamp + \".\" + event_id + \".\" + raw_body",
},
"dedupe_keys": []string{"X-Agnet-Event-Id", "event_id", "idempotency_key"},
"dedupe_keys": []string{"X-Agent-Event-Id", "event_id", "idempotency_key"},
"events": items,
"security": gin.H{
"plaintext_secrets_allowed": false,
@@ -547,7 +547,7 @@ func AgnetGetSwarmEventCallbackSchema(c *gin.Context) {
})
}
func callbackEnvelopeFieldValue(payload agnetCallbackEnvelope, key string) string {
func callbackEnvelopeFieldValue(payload agentCallbackEnvelope, key string) string {
switch key {
case "task_id":
return strings.TrimSpace(payload.TaskID)
@@ -558,7 +558,7 @@ func callbackEnvelopeFieldValue(payload agnetCallbackEnvelope, key string) strin
}
}
func callbackHasFieldValue(payload agnetCallbackEnvelope, key string) bool {
func callbackHasFieldValue(payload agentCallbackEnvelope, key string) bool {
if callbackEnvelopeFieldValue(payload, key) != "" {
return true
}
@@ -578,8 +578,8 @@ func callbackHasFieldValue(payload agnetCallbackEnvelope, key string) bool {
return false
}
func validateAgnetCallbackEventSchema(payload agnetCallbackEnvelope) error {
required, ok := agnetCallbackEventRequiredFields[payload.EventType]
func validateAgentCallbackEventSchema(payload agentCallbackEnvelope) error {
required, ok := agentCallbackEventRequiredFields[payload.EventType]
if !ok {
return nil
}
@@ -591,7 +591,7 @@ func validateAgnetCallbackEventSchema(payload agnetCallbackEnvelope) error {
return nil
}
func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agnetDeploymentRecord) error {
func persistAgentApprovalFromCallback(payload agentCallbackEnvelope, record agentDeploymentRecord) error {
if payload.EventType != "approval.requested" || model.DB == nil {
return nil
}
@@ -601,7 +601,7 @@ func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agne
}
approvalID := firstNonEmpty(callbackStringValue(source, "approval_id"), "appr_"+common.GetUUID())
userID, _ := strconv.Atoi(strings.TrimSpace(record.Plan.UserContext.UserID))
approval := model.AgnetApprovalRequest{
approval := model.AgentApprovalRequest{
ApprovalID: approvalID,
UserId: userID,
DeploymentID: strings.TrimSpace(payload.DeploymentID),
@@ -614,16 +614,16 @@ func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agne
RiskLevel: firstNonEmpty(callbackStringValue(source, "risk_level"), "high"),
RequiresCredential: callbackBoolValue(source, "requires_credential"),
SecretRef: strings.TrimSpace(callbackStringValue(source, "secret_ref")),
Status: agnetApprovalStatusPending,
RequestedBy: firstNonEmpty(callbackStringValue(source, "requested_by"), "agnet-runtime"),
Status: agentApprovalStatusPending,
RequestedBy: firstNonEmpty(callbackStringValue(source, "requested_by"), "agent-runtime"),
RequestReason: firstNonEmpty(callbackStringValue(source, "reason"), callbackStringValue(source, "summary"), "Runtime requested approval"),
TTLSeconds: callbackIntValue(source, "ttl_seconds"),
}
if approval.TTLSeconds <= 0 {
approval.TTLSeconds = defaultAgnetApprovalTTLSeconds
approval.TTLSeconds = defaultAgentApprovalTTLSeconds
}
if approval.TTLSeconds > maxAgnetApprovalTTLSeconds {
approval.TTLSeconds = maxAgnetApprovalTTLSeconds
if approval.TTLSeconds > maxAgentApprovalTTLSeconds {
approval.TTLSeconds = maxAgentApprovalTTLSeconds
}
if approval.SecretRef != "" && !strings.HasPrefix(approval.SecretRef, "azkv://") {
return fmt.Errorf("approval secret_ref must use azkv:// Azure Key Vault reference")
@@ -634,59 +634,59 @@ func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agne
now := time.Now().UnixMilli()
approval.ExpiresAt = now + int64(approval.TTLSeconds)*1000
var existing model.AgnetApprovalRequest
var existing model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ?", approval.ApprovalID).First(&existing).Error; err == nil {
return nil
}
if err := model.DB.Create(&approval).Error; err != nil {
return err
}
recordAgnetApprovalAudit("approval.requested", &approval, nil, "ok", "")
recordAgentApprovalAudit("approval.requested", &approval, nil, "ok", "")
return nil
}
func AgnetReceiveSwarmEventCallback(c *gin.Context) {
func AgentReceiveRuntimeEventCallback(c *gin.Context) {
rawBody, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20))
if err != nil {
agnetError(c, "CALLBACK_INVALID", "failed to read callback body")
agentError(c, "CALLBACK_INVALID", "failed to read callback body")
return
}
var payload agnetCallbackEnvelope
var payload agentCallbackEnvelope
if err := common.Unmarshal(rawBody, &payload); err != nil {
agnetError(c, "CALLBACK_INVALID", err.Error())
agentError(c, "CALLBACK_INVALID", err.Error())
return
}
if payload.EventID == "" {
payload.EventID = strings.TrimSpace(c.GetHeader("X-Agnet-Event-Id"))
payload.EventID = strings.TrimSpace(c.GetHeader("X-Agent-Event-Id"))
}
if payload.CorrelationID == "" {
payload.CorrelationID = strings.TrimSpace(c.GetHeader("X-Correlation-ID"))
}
payload.EventID = strings.TrimSpace(payload.EventID)
payload.EventType = strings.TrimSpace(payload.EventType)
if !validateAgnetCallbackAuth(c, rawBody, payload.EventID) {
if !validateAgentCallbackAuth(c, rawBody, payload.EventID) {
return
}
if payload.EventID == "" || payload.EventType == "" {
agnetError(c, "CALLBACK_INVALID", "event_id and event_type are required")
agentError(c, "CALLBACK_INVALID", "event_id and event_type are required")
return
}
normalizeCallbackArtifact(&payload)
if payload.IdempotencyKey == "" {
payload.IdempotencyKey = payload.EventID
}
if err := validateAgnetCallbackEventSchema(payload); err != nil {
agnetError(c, "CALLBACK_SCHEMA_INVALID", err.Error())
if err := validateAgentCallbackEventSchema(payload); err != nil {
agentError(c, "CALLBACK_SCHEMA_INVALID", err.Error())
return
}
if agnetCallbackHasPlaintextSecret(payload) {
agnetError(c, "CALLBACK_SECRET_REJECTED", "callbacks must not contain plaintext credential fields")
if agentCallbackHasPlaintextSecret(payload) {
agentError(c, "CALLBACK_SECRET_REJECTED", "callbacks must not contain plaintext credential fields")
return
}
incomingDeploymentID := strings.TrimSpace(payload.DeploymentID)
incomingSwarmID := strings.TrimSpace(payload.SwarmID)
record, _ := agnetCallbackDeploymentContext(incomingDeploymentID, incomingSwarmID)
record, _ := agentCallbackDeploymentContext(incomingDeploymentID, incomingSwarmID)
if strings.TrimSpace(record.DeploymentID) != "" {
payload.DeploymentID = record.DeploymentID
if strings.TrimSpace(payload.SwarmID) == "" {
@@ -697,7 +697,7 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
payload.CorrelationID = record.Plan.Metadata.CorrelationID
}
payloadJSON, _ := common.Marshal(payload)
inserted, err := model.InsertAgnetCallbackEvent(&model.AgnetCallbackEvent{
inserted, err := model.InsertAgentCallbackEvent(&model.AgentCallbackEvent{
EventID: payload.EventID,
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
CallbackType: "swarm-event",
@@ -716,23 +716,23 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
CreatedAtMs: time.Now().UnixMilli(),
})
if err != nil {
common.SysLog("AgnetReceiveSwarmEventCallback: " + err.Error())
agnetError(c, "CALLBACK_PERSIST_FAILED", "failed to persist callback")
common.SysLog("AgentReceiveRuntimeEventCallback: " + err.Error())
agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist callback")
return
}
if inserted {
if err := persistAgnetArtifactFromCallback(payload, record); err != nil {
common.SysLog("persistAgnetArtifactFromCallback: " + err.Error())
agnetError(c, "CALLBACK_PERSIST_FAILED", "failed to persist artifact")
if err := persistAgentArtifactFromCallback(payload, record); err != nil {
common.SysLog("persistAgentArtifactFromCallback: " + err.Error())
agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist artifact")
return
}
if err := persistAgnetApprovalFromCallback(payload, record); err != nil {
common.SysLog("persistAgnetApprovalFromCallback: " + err.Error())
agnetError(c, "CALLBACK_PERSIST_FAILED", "failed to persist approval request")
if err := persistAgentApprovalFromCallback(payload, record); err != nil {
common.SysLog("persistAgentApprovalFromCallback: " + err.Error())
agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist approval request")
return
}
record, _ = applyAgnetCallbackDeploymentState(payload, record)
recordAgnetAuditEvent(agnetEvent{
record, _ = applyAgentCallbackDeploymentState(payload, record)
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "callback." + payload.EventType,
SchemaVersion: 1,
@@ -741,8 +741,8 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
BindingScope: firstPlanBindingScope(record.Plan),
DeploymentID: strings.TrimSpace(payload.DeploymentID),
CorrelationID: strings.TrimSpace(payload.CorrelationID),
OccurredAt: firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agnetNow()),
}, "agnet_callback", strings.TrimSpace(payload.DeploymentID), agnetRequestID(c), "ok")
OccurredAt: firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agentNow()),
}, "agent_callback", strings.TrimSpace(payload.DeploymentID), agentRequestID(c), "ok")
}
common.ApiSuccess(c, gin.H{
"event_id": payload.EventID,
@@ -753,54 +753,54 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
})
}
func AgnetListUserDeploymentArtifacts(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentListUserDeploymentArtifacts(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
items, err := model.ListAgnetArtifacts(model.ListAgnetArtifactsFilter{
items, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
if err != nil {
common.SysLog("AgnetListUserDeploymentArtifacts: " + err.Error())
agnetError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifacts")
common.SysLog("AgentListUserDeploymentArtifacts: " + err.Error())
agentError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifacts")
return
}
common.ApiSuccess(c, gin.H{"deployment_id": record.DeploymentID, "artifacts": items, "items": items, "total": len(items)})
}
func AgnetGetUserDeploymentArtifactContent(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentGetUserDeploymentArtifactContent(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
artifactID := strings.TrimSpace(c.Param("artifact_id"))
if artifactID == "" {
agnetError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
return
}
artifact, found, err := model.GetAgnetArtifactByDeployment(record.DeploymentID, artifactID)
artifact, found, err := model.GetAgentArtifactByDeployment(record.DeploymentID, artifactID)
if err != nil {
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
agnetError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifact")
common.SysLog("AgentGetUserDeploymentArtifactContent: " + err.Error())
agentError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifact")
return
}
if !found {
agnetError(c, "ARTIFACT_NOT_FOUND", "artifact not found")
agentError(c, "ARTIFACT_NOT_FOUND", "artifact not found")
return
}
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
agnetError(c, "RUNTIME_NOT_CONFIGURED", "runtime is not configured")
agentError(c, "RUNTIME_NOT_CONFIGURED", "runtime is not configured")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
resp, err := callAgnetRuntimeArtifactContent(ctx, cfg, record, artifact.ArtifactID)
resp, err := callAgentRuntimeArtifactContent(ctx, cfg, record, artifact.ArtifactID)
if err != nil {
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
agnetError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content")
common.SysLog("AgentGetUserDeploymentArtifactContent: " + err.Error())
agentError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content")
return
}
defer resp.Body.Close()
@@ -818,8 +818,8 @@ func AgnetGetUserDeploymentArtifactContent(c *gin.Context) {
c.DataFromReader(http.StatusOK, resp.ContentLength, contentType, resp.Body, headers)
}
func callbackPayloadMap(row model.AgnetCallbackEvent) map[string]any {
var payload agnetCallbackEnvelope
func callbackPayloadMap(row model.AgentCallbackEvent) map[string]any {
var payload agentCallbackEnvelope
if err := common.UnmarshalJsonStr(row.PayloadJSON, &payload); err != nil {
return nil
}
@@ -829,7 +829,7 @@ func callbackPayloadMap(row model.AgnetCallbackEvent) map[string]any {
return nil
}
func timelineEntryFromCallback(callback model.AgnetCallbackEvent) gin.H {
func timelineEntryFromCallback(callback model.AgentCallbackEvent) gin.H {
payload := callbackPayloadMap(callback)
entry := gin.H{
"kind": "callback",
@@ -852,29 +852,29 @@ func timelineEntryFromCallback(callback model.AgnetCallbackEvent) gin.H {
return entry
}
func AgnetGetUserDeploymentTimeline(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentGetUserDeploymentTimeline(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
events, err := model.ListAgnetAuditEventsByDeployment(record.DeploymentID)
events, err := model.ListAgentAuditEventsByDeployment(record.DeploymentID)
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query audit events")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query audit events")
return
}
callbacks, err := model.ListAgnetCallbackEvents(model.ListAgnetCallbackEventsFilter{DeploymentID: record.DeploymentID, Limit: 500})
callbacks, err := model.ListAgentCallbackEvents(model.ListAgentCallbackEventsFilter{DeploymentID: record.DeploymentID, Limit: 500})
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query callbacks")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query callbacks")
return
}
artifacts, err := model.ListAgnetArtifacts(model.ListAgnetArtifactsFilter{DeploymentID: record.DeploymentID, Limit: 500})
artifacts, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{DeploymentID: record.DeploymentID, Limit: 500})
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query artifacts")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query artifacts")
return
}
snapshots, err := model.ListAgnetSKSnapshots(record.DeploymentID)
snapshots, err := model.ListAgentSKSnapshots(record.DeploymentID)
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query sk snapshots")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query sk snapshots")
return
}
timeline := make([]gin.H, 0, len(events)+len(callbacks)+len(artifacts)+len(snapshots))
@@ -892,7 +892,7 @@ func AgnetGetUserDeploymentTimeline(c *gin.Context) {
}
common.ApiSuccess(c, gin.H{
"deployment_id": record.DeploymentID,
"deployment": record,
"deployment": withDisplayStatus(record),
"events": events,
"callbacks": callbacks,
"artifacts": artifacts,
@@ -132,21 +132,21 @@ func TestValueLooksLikeSecretAndPlaintextScan(t *testing.T) {
}
// P5: a single source of truth for the default model; no placeholder fallback.
func TestDefaultAgnetModelID_SingleSourceNoPlaceholder(t *testing.T) {
def := defaultAgnetModelID()
func TestDefaultAgentModelID_SingleSourceNoPlaceholder(t *testing.T) {
def := defaultAgentModelID()
require.Equal(t, "gpt-5.4", def)
// Draft builder must use the single default, never agnet-model-<role>.
plan := buildAgnetDraftAgentPlan("backend", "", nil)
// Draft builder must use the single default, never agent-model-<role>.
plan := buildAgentDraftAgentPlan("backend", "", nil)
require.Equal(t, def, plan.DefaultModelID)
require.NotContains(t, plan.DefaultModelID, "agnet-model-")
require.NotContains(t, plan.DefaultModelID, "agent-model-")
// Explicit client model is still honored.
plan = buildAgnetDraftAgentPlan("backend", "gpt-5.4-mini", nil)
plan = buildAgentDraftAgentPlan("backend", "gpt-5.4-mini", nil)
require.Equal(t, "gpt-5.4-mini", plan.DefaultModelID)
// Every role template resolves to the single default, no claude-* hardcoding.
for _, tpl := range agnetRoleTemplates() {
for _, tpl := range agentRoleTemplates() {
require.Equal(t, def, tpl.DefaultModel, "role %s", tpl.Key)
}
}
@@ -5,7 +5,7 @@ import (
"github.com/heicode/manager/common"
)
// AgnetRoleTemplate is the platform-recommended role catalog Manager
// AgentRoleTemplate is the platform-recommended role catalog Manager
// surfaces to users when they assemble an AI development team. The
// six canonical roles come from docs/product-package/13-platform-
// description.md §3 and 04-platform-usage-guide.md §第五步.
@@ -14,7 +14,7 @@ import (
// - Roles are platform-defined contracts, not user-editable data.
// Treating them like rows would invite drift between deployments.
// - Permission hints below are *recommendations* the UI uses to
// pre-fill the "what can this Agnet do" confirmation card —
// pre-fill the "what can this Agent do" confirmation card —
// the actual permission grant still goes through ResourceGrant.
// - If we ever need per-tenant role customization, we add an
// overlay table; the canonical set still lives here as the
@@ -24,7 +24,7 @@ import (
// across persistence and the client picker. Display strings can
// be translated, but the key must NEVER change without a coordinated
// frontend rollout.
type AgnetRoleTemplate struct {
type AgentRoleTemplate struct {
Key string `json:"key"`
DisplayName string `json:"display_name"`
Summary string `json:"summary"`
@@ -33,132 +33,132 @@ type AgnetRoleTemplate struct {
RiskLevel string `json:"risk_level"`
}
// defaultAgnetModelID is the single source of truth for the default sub-agent
// defaultAgentModelID is the single source of truth for the default sub-agent
// model. It is aligned to the production-verified NewAPI model and overridable
// via AGNET_DEFAULT_MODEL_ID, so role templates, deployment drafts and runtime
// agent refs never fall back to placeholder names (e.g. agnet-model-<role>)
// via AGENT_DEFAULT_MODEL_ID, so role templates, deployment drafts and runtime
// agent refs never fall back to placeholder names (e.g. agent-model-<role>)
// that production NewAPI cannot route ("No available channel for model ...").
func defaultAgnetModelID() string {
return common.GetEnvOrDefaultString("AGNET_DEFAULT_MODEL_ID", "gpt-5.4")
func defaultAgentModelID() string {
return common.GetEnvOrDefaultString("AGENT_DEFAULT_MODEL_ID", "gpt-5.4")
}
// agnetRoleTemplates returns the canonical six-role catalog. Order
// agentRoleTemplates returns the canonical six-role catalog. Order
// matches the typical lifecycle a user walks through when assembling
// a team: discover -> design -> build -> review -> operate.
//
// Permission hints use the verbs from docs §13.3.4 (Resource Grant)
// and stay deliberately broad — concrete grants come from the user
// resource-binding flow.
func agnetRoleTemplates() []AgnetRoleTemplate {
return []AgnetRoleTemplate{
func agentRoleTemplates() []AgentRoleTemplate {
return []AgentRoleTemplate{
{
Key: "product",
DisplayName: "Product Agnet",
DisplayName: "Product Agent",
Summary: "Refines the user idea into product scope, requirements and acceptance criteria.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:project_docs",
"write:product_spec",
},
RiskLevel: agnetRiskLow,
RiskLevel: agentRiskLow,
},
{
Key: "architect",
DisplayName: "Architect Agnet",
DisplayName: "Architect Agent",
Summary: "Designs the technical approach, picks frameworks, and breaks work into sub-tasks.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"write:architecture_doc",
},
RiskLevel: agnetRiskLow,
RiskLevel: agentRiskLow,
},
{
Key: "frontend",
DisplayName: "Frontend Agnet",
DisplayName: "Frontend Agent",
Summary: "Implements UI, components and client-side state per the architect's plan.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"write:repo:frontend",
},
RiskLevel: agnetRiskMedium,
RiskLevel: agentRiskMedium,
},
{
Key: "backend",
DisplayName: "Backend Agnet",
DisplayName: "Backend Agent",
Summary: "Implements server-side APIs, data models and integrations.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"write:repo:backend",
"read:dev_database",
},
RiskLevel: agnetRiskMedium,
RiskLevel: agentRiskMedium,
},
{
Key: "reviewer",
DisplayName: "Reviewer Agnet",
DisplayName: "Reviewer Agent",
Summary: "Performs code review, security checks and runs the test suite.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"run:tests",
"comment:pull_request",
},
RiskLevel: agnetRiskLow,
RiskLevel: agentRiskLow,
},
{
Key: "ops",
DisplayName: "Ops Agnet",
DisplayName: "Ops Agent",
Summary: "Deploys to test environments, watches logs and prepares production rollouts (production requires approval).",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"deploy:test_env",
"read:metrics",
"approval_required:deploy_prod",
},
RiskLevel: agnetRiskHigh,
RiskLevel: agentRiskHigh,
},
}
}
// AgnetListRoleTemplates is the GET /api/agnet/role-templates handler.
// AgentListRoleTemplates is the GET /api/agent/role-templates handler.
// Returns the canonical six-role catalog so the deployment-creation
// UI can pre-populate role pickers and the documentation page can
// render the role overview.
//
// Auth: requires UserAuth (mounted by router). Anyone logged in to
// Manager can read the catalog; there are no secrets in the payload.
func AgnetListRoleTemplates(c *gin.Context) {
tpls := agnetRoleTemplates()
func AgentListRoleTemplates(c *gin.Context) {
tpls := agentRoleTemplates()
common.ApiSuccess(c, gin.H{
"items": tpls,
"total": len(tpls),
})
}
// agnetRoleTemplateKeys is a helper for validation in deployment
// agentRoleTemplateKeys is a helper for validation in deployment
// creation — checks whether a user-provided role_template string is
// one of the canonical six. Returns true for any of the canonical
// keys; returns true for unknown keys too (deployment flow today
// accepts free-form role_template strings, see agnet_control_plane.
// accepts free-form role_template strings, see agent_control_plane.
// go:552), so this helper is currently advisory. When we tighten
// validation (after frontend ships the new picker), flip the
// fallback to false and add a unit test.
func agnetRoleTemplateKeys() map[string]bool {
func agentRoleTemplateKeys() map[string]bool {
keys := make(map[string]bool)
for _, t := range agnetRoleTemplates() {
for _, t := range agentRoleTemplates() {
keys[t.Key] = true
}
return keys
}
// agnetIsCanonicalRoleKey reports whether `key` matches one of the
// agentIsCanonicalRoleKey reports whether `key` matches one of the
// six platform-defined roles. Today the deployment endpoint accepts
// any non-empty string; this helper is reserved for the next step
// when we move to a closed set.
func agnetIsCanonicalRoleKey(key string) bool {
return agnetRoleTemplateKeys()[key]
func agentIsCanonicalRoleKey(key string) bool {
return agentRoleTemplateKeys()[key]
}
@@ -9,13 +9,13 @@ import (
"github.com/heicode/manager/common"
)
func TestAgnetRoleTemplates_CanonicalSetCovered(t *testing.T) {
func TestAgentRoleTemplates_CanonicalSetCovered(t *testing.T) {
// Pins the six canonical role keys from docs §13.3.3. Any code
// change that adds, removes or renames a key MUST update this
// list — guards against accidental drift between Manager and
// the product spec.
want := []string{"product", "architect", "frontend", "backend", "reviewer", "ops"}
got := agnetRoleTemplates()
got := agentRoleTemplates()
if len(got) != len(want) {
t.Fatalf("expected %d roles, got %d", len(want), len(got))
}
@@ -37,7 +37,7 @@ func TestAgnetRoleTemplates_CanonicalSetCovered(t *testing.T) {
}
}
func TestAgnetRoleTemplates_RiskLevels(t *testing.T) {
func TestAgentRoleTemplates_RiskLevels(t *testing.T) {
// Ops is the only canonical role with high risk (production
// deployment intent). Reviewer + Product + Architect stay low
// (read-mostly). Frontend + Backend land at medium. Pins the
@@ -45,29 +45,29 @@ func TestAgnetRoleTemplates_RiskLevels(t *testing.T) {
// flip an ops role to "low" and skip the high-risk approval
// gating downstream.
wantRisk := map[string]string{
"product": agnetRiskLow,
"architect": agnetRiskLow,
"reviewer": agnetRiskLow,
"frontend": agnetRiskMedium,
"backend": agnetRiskMedium,
"ops": agnetRiskHigh,
"product": agentRiskLow,
"architect": agentRiskLow,
"reviewer": agentRiskLow,
"frontend": agentRiskMedium,
"backend": agentRiskMedium,
"ops": agentRiskHigh,
}
for _, tpl := range agnetRoleTemplates() {
for _, tpl := range agentRoleTemplates() {
if want, ok := wantRisk[tpl.Key]; ok && tpl.RiskLevel != want {
t.Errorf("role %q: want risk %q, got %q", tpl.Key, want, tpl.RiskLevel)
}
}
}
func TestAgnetListRoleTemplates_HTTPShape(t *testing.T) {
func TestAgentListRoleTemplates_HTTPShape(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest("GET", "/api/agnet/role-templates", nil)
c.Request = httptest.NewRequest("GET", "/api/agent/role-templates", nil)
c.Set("id", 1)
c.Set("role", common.RoleCommonUser)
AgnetListRoleTemplates(c)
AgentListRoleTemplates(c)
if rec.Code != 200 {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
@@ -83,17 +83,17 @@ func TestAgnetListRoleTemplates_HTTPShape(t *testing.T) {
}
}
func TestAgnetIsCanonicalRoleKey(t *testing.T) {
func TestAgentIsCanonicalRoleKey(t *testing.T) {
// Defensive helper currently used as advisory — pins the closed
// set so the future tighten-up to closed-set validation is one
// flip instead of an open-ended audit.
for _, ok := range []string{"product", "architect", "frontend", "backend", "reviewer", "ops"} {
if !agnetIsCanonicalRoleKey(ok) {
if !agentIsCanonicalRoleKey(ok) {
t.Errorf("%q should be canonical", ok)
}
}
for _, bad := range []string{"", "debugger", "executor", "random_string"} {
if agnetIsCanonicalRoleKey(bad) {
if agentIsCanonicalRoleKey(bad) {
t.Errorf("%q should NOT be canonical", bad)
}
}
@@ -17,15 +17,15 @@ import (
)
const (
agnetRuntimeStateSyncing = "runtime_syncing"
agnetRuntimeStateSynced = "runtime_accepted"
agnetRuntimeStateFailed = "runtime_sync_failed"
agentRuntimeStateSyncing = "runtime_syncing"
agentRuntimeStateSynced = "runtime_accepted"
agentRuntimeStateFailed = "runtime_sync_failed"
agnetRuntimeModeAgnet = "agnet"
agnetRuntimeModeSwarm = "swarm"
agentRuntimeModeAgent = "agent"
agentRuntimeModeSwarm = "swarm"
)
type agnetRuntimeConfig struct {
type agentRuntimeConfig struct {
Enabled bool
Async bool
BaseURL string
@@ -39,14 +39,14 @@ type agnetRuntimeConfig struct {
Timeout time.Duration
}
type agnetRuntimeSyncResult struct {
type agentRuntimeSyncResult struct {
RuntimeDeploymentID string
RuntimeSwarmID string
RuntimeStatus string
RawStatusCode int
}
type agnetRuntimeDiagnostics struct {
type agentRuntimeDiagnostics struct {
DeploymentID string `json:"deployment_id"`
RuntimeMode string `json:"runtime_mode"`
SubMode string `json:"sub_mode"`
@@ -65,41 +65,42 @@ type agnetRuntimeDiagnostics struct {
CheckedAt string `json:"checked_at"`
}
func normalizeAgnetRuntimeMode(value string) string {
func normalizeAgentRuntimeMode(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case agnetRuntimeModeSwarm:
return agnetRuntimeModeSwarm
case agentRuntimeModeSwarm:
return agentRuntimeModeSwarm
default:
return agnetRuntimeModeAgnet
return agentRuntimeModeAgent
}
}
func agnetRuntimeModeForSource(source string) string {
func agentRuntimeModeForSource(source string) string {
if strings.TrimSpace(source) == "api_swarms_adapter" {
return agnetRuntimeModeSwarm
return agentRuntimeModeSwarm
}
return agnetRuntimeModeAgnet
return agentRuntimeModeAgent
}
func agnetRuntimeModeForRecord(record agnetDeploymentRecord) string {
return normalizeAgnetRuntimeMode(record.Plan.Metadata.RuntimeMode)
func agentRuntimeModeForRecord(record agentDeploymentRecord) string {
return normalizeAgentRuntimeMode(record.Plan.Metadata.RuntimeMode)
}
func agnetRuntimeClientConfig() agnetRuntimeConfig {
return agnetRuntimeClientConfigForMode(agnetRuntimeModeAgnet)
func agentRuntimeClientConfig() agentRuntimeConfig {
return agentRuntimeClientConfigForMode(agentRuntimeModeAgent)
}
func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
timeoutSec := common.GetEnvOrDefault("AGNET_RUNTIME_TIMEOUT_SECONDS", 5)
func agentRuntimeClientConfigForMode(mode string) agentRuntimeConfig {
timeoutSec := common.GetEnvOrDefault("AGENT_RUNTIME_TIMEOUT_SECONDS", 5)
if timeoutSec <= 0 {
timeoutSec = 5
}
mode = normalizeAgnetRuntimeMode(mode)
prefix := "AGNET_RUNTIME_"
defaultCreatePath := "/api/agnet/deployments"
defaultStopPath := "/api/agnet/deployments/{deployment_id}/stop"
mode = normalizeAgentRuntimeMode(mode)
prefix := "AGENT_RUNTIME_"
// Sub Agile primary route per agent_management Sub Mode Runtime §2.2.
defaultCreatePath := "/api/agent/sub-agile/deployments"
defaultStopPath := "/api/agent/sub-agile/deployments/{deployment_id}/stop"
defaultApprovalPath := "/api/swarms/{swarm_id}/approvals/{approval_id}"
if mode == agnetRuntimeModeSwarm {
if mode == agentRuntimeModeSwarm {
prefix = "SWARM_RUNTIME_"
defaultCreatePath = "/api/swarms"
defaultStopPath = "/api/swarms/{swarm_id}/stop"
@@ -110,18 +111,18 @@ func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
}
baseURL := strings.TrimRight(strings.TrimSpace(common.GetEnvOrDefaultString(prefix+"BASE_URL", "")), "/")
enabledDefault := false
if mode == agnetRuntimeModeAgnet {
enabledDefault = common.GetEnvOrDefaultBool("AGNET_RUNTIME_ENABLED", false)
if mode == agentRuntimeModeAgent {
enabledDefault = common.GetEnvOrDefaultBool("AGENT_RUNTIME_ENABLED", false)
} else {
enabledDefault = baseURL != ""
}
return agnetRuntimeConfig{
return agentRuntimeConfig{
Enabled: common.GetEnvOrDefaultBool(prefix+"ENABLED", enabledDefault),
Async: common.GetEnvOrDefaultBool(prefix+"ASYNC", common.GetEnvOrDefaultBool("AGNET_RUNTIME_ASYNC", true)),
Async: common.GetEnvOrDefaultBool(prefix+"ASYNC", common.GetEnvOrDefaultBool("AGENT_RUNTIME_ASYNC", true)),
BaseURL: baseURL,
Token: strings.TrimSpace(common.GetEnvOrDefaultString(prefix+"SERVICE_TOKEN", "")),
CreatePath: common.GetEnvOrDefaultString(prefix+"CREATE_PATH", defaultCreatePath),
HealthPath: common.GetEnvOrDefaultString(prefix+"HEALTH_PATH", "/api/agnet/health"),
HealthPath: common.GetEnvOrDefaultString(prefix+"HEALTH_PATH", "/api/agent/health"),
StatusPath: common.GetEnvOrDefaultString(prefix+"STATUS_PATH", "/api/swarms/{swarm_id}/status"),
ArtifactContentPath: common.GetEnvOrDefaultString(prefix+"ARTIFACT_CONTENT_PATH", "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"),
StopPath: common.GetEnvOrDefaultString(prefix+"STOP_PATH", defaultStopPath),
@@ -130,19 +131,19 @@ func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
}
}
func agnetRuntimeCallbackURL() string {
if value := strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_CALLBACK_URL", "")); value != "" {
func agentRuntimeCallbackURL() string {
if value := strings.TrimSpace(common.GetEnvOrDefaultString("AGENT_RUNTIME_CALLBACK_URL", "")); value != "" {
return value
}
baseURL := strings.TrimRight(strings.TrimSpace(common.GetEnvOrDefaultString("HEICODE_PUBLIC_BASE_URL", "https://code.xinghanlab.com")), "/")
return baseURL + "/api/agnet/callbacks/swarm-events"
return baseURL + "/api/agent/callbacks/runtime-events"
}
func agnetRuntimeCallbackSigningSecretRef() string {
return strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""))
func agentRuntimeCallbackSigningSecretRef() string {
return strings.TrimSpace(common.GetEnvOrDefaultString("AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""))
}
func agnetRuntimeSubscribedEvents() []string {
func agentRuntimeSubscribedEvents() []string {
return []string{
"deployment.status_changed",
"phase.changed",
@@ -178,7 +179,7 @@ func firstNonEmpty(values ...string) string {
return ""
}
func truncateAgnetFailureReason(value string) string {
func truncateAgentFailureReason(value string) string {
value = strings.TrimSpace(value)
if len(value) <= 480 {
return value
@@ -186,13 +187,13 @@ func truncateAgnetFailureReason(value string) string {
return value[:480]
}
func agnetRuntimeURL(baseURL string, path string) (string, error) {
func agentRuntimeURL(baseURL string, path string) (string, error) {
if strings.TrimSpace(baseURL) == "" {
return "", errors.New("AGNET_RUNTIME_BASE_URL is not configured")
return "", errors.New("AGENT_RUNTIME_BASE_URL is not configured")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", errors.New("AGNET_RUNTIME_BASE_URL must be an absolute http(s) URL")
return "", errors.New("AGENT_RUNTIME_BASE_URL must be an absolute http(s) URL")
}
if strings.TrimSpace(path) == "" {
path = "/"
@@ -203,7 +204,7 @@ func agnetRuntimeURL(baseURL string, path string) (string, error) {
return strings.TrimRight(baseURL, "/") + path, nil
}
func agnetRuntimeHeaders(req *http.Request, cfg agnetRuntimeConfig, record agnetDeploymentRecord) {
func agentRuntimeHeaders(req *http.Request, cfg agentRuntimeConfig, record agentDeploymentRecord) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-ID", record.Plan.UserContext.UserID)
req.Header.Set("X-Binding-Scope", firstPlanBindingScope(record.Plan))
@@ -214,8 +215,8 @@ func agnetRuntimeHeaders(req *http.Request, cfg agnetRuntimeConfig, record agnet
}
}
func agnetRuntimeRequestAgents(plan agnetOrchestrationPlan) []gin.H {
byRole := make(map[string]agnetAgentPlan, len(plan.Agents))
func agentRuntimeRequestAgents(plan agentOrchestrationPlan) []gin.H {
byRole := make(map[string]agentAgentPlan, len(plan.Agents))
for _, agent := range plan.Agents {
role := strings.TrimSpace(agent.RoleTemplate)
if role != "" {
@@ -235,7 +236,7 @@ func agnetRuntimeRequestAgents(plan agnetOrchestrationPlan) []gin.H {
item["sk_sources"] = agent.SKSources
}
if len(agent.ResourceGrants) > 0 {
item["resource_grants"] = agnetRuntimeResourceGrantPayloads(agent.ResourceGrants)
item["resource_grants"] = agentRuntimeResourceGrantPayloads(agent.ResourceGrants)
}
}
items = append(items, item)
@@ -254,14 +255,14 @@ func agnetRuntimeRequestAgents(plan agnetOrchestrationPlan) []gin.H {
item["sk_sources"] = agent.SKSources
}
if len(agent.ResourceGrants) > 0 {
item["resource_grants"] = agnetRuntimeResourceGrantPayloads(agent.ResourceGrants)
item["resource_grants"] = agentRuntimeResourceGrantPayloads(agent.ResourceGrants)
}
items = append(items, item)
}
return items
}
func agnetRuntimeRequestSwarmAgents(plan agnetOrchestrationPlan) []gin.H {
func agentRuntimeRequestSwarmAgents(plan agentOrchestrationPlan) []gin.H {
runtimeModels := make(map[string]string, len(plan.AgentRuntime.Agents))
for _, runtimeAgent := range plan.AgentRuntime.Agents {
role := strings.TrimSpace(runtimeAgent.Role)
@@ -276,7 +277,7 @@ func agnetRuntimeRequestSwarmAgents(plan agnetOrchestrationPlan) []gin.H {
if role == "" {
continue
}
taskID := fmt.Sprintf("%s-%d", sanitizeAgnetRef(role), index+1)
taskID := fmt.Sprintf("%s-%d", sanitizeAgentRef(role), index+1)
item := gin.H{
"task_id": taskID,
"role": role,
@@ -291,14 +292,14 @@ func agnetRuntimeRequestSwarmAgents(plan agnetOrchestrationPlan) []gin.H {
item["sk_sources"] = agent.SKSources
}
if len(agent.ResourceGrants) > 0 {
item["resource_grants"] = agnetRuntimeResourceGrantPayloads(agent.ResourceGrants)
item["resource_grants"] = agentRuntimeResourceGrantPayloads(agent.ResourceGrants)
}
items = append(items, item)
}
return items
}
func agnetRuntimeResourceGrantPayloads(grants []agnetResourceGrant) []gin.H {
func agentRuntimeResourceGrantPayloads(grants []agentResourceGrant) []gin.H {
items := make([]gin.H, 0, len(grants))
for _, grant := range grants {
secretRef := strings.TrimSpace(grant.SecretRef)
@@ -319,8 +320,8 @@ func agnetRuntimeResourceGrantPayloads(grants []agnetResourceGrant) []gin.H {
"status": grant.Status,
"ref": secretRef,
"secret_ref": secretRef,
"allowed_ref": agnetGrantResourceRef(grant),
"resource_ref_hint": agnetGrantResourceRef(grant),
"allowed_ref": agentGrantResourceRef(grant),
"resource_ref_hint": agentGrantResourceRef(grant),
}
if grant.TenantID != "" {
item["tenant_id"] = grant.TenantID
@@ -342,23 +343,23 @@ func agnetRuntimeResourceGrantPayloads(grants []agnetResourceGrant) []gin.H {
return items
}
func agnetRuntimeRequestResourceGrants(plan agnetOrchestrationPlan) []gin.H {
items := make([]agnetResourceGrant, 0, len(plan.ResourceGrants))
func agentRuntimeRequestResourceGrants(plan agentOrchestrationPlan) []gin.H {
items := make([]agentResourceGrant, 0, len(plan.ResourceGrants))
items = append(items, plan.ResourceGrants...)
for _, agent := range plan.Agents {
items = append(items, agent.ResourceGrants...)
}
return agnetRuntimeResourceGrantPayloads(items)
return agentRuntimeResourceGrantPayloads(items)
}
func agnetRuntimeRequestMetadata(record agnetDeploymentRecord, source string) gin.H {
func agentRuntimeRequestMetadata(record agentDeploymentRecord, source string) gin.H {
metadata := gin.H{
"correlation_id": record.Plan.Metadata.CorrelationID,
"manager_deployment_id": record.DeploymentID,
"source": source,
"heicode_deployment_id": record.DeploymentID,
"heicode_runtime_bridge": true,
"runtime_mode": agnetRuntimeModeForRecord(record),
"runtime_mode": agentRuntimeModeForRecord(record),
}
if record.Plan.Metadata.TenantID != "" {
metadata["tenant_id"] = record.Plan.Metadata.TenantID
@@ -369,7 +370,7 @@ func agnetRuntimeRequestMetadata(record agnetDeploymentRecord, source string) gi
return metadata
}
func agnetRuntimeBudgetPayload(budget agnetBudget) gin.H {
func agentRuntimeBudgetPayload(budget agentBudget) gin.H {
return gin.H{
"max_tokens": budget.MaxTokens,
"token_limit": budget.MaxTokens,
@@ -380,8 +381,8 @@ func agnetRuntimeBudgetPayload(budget agnetBudget) gin.H {
}
}
func agnetRuntimeOrchestrationPlanPayload(record agnetDeploymentRecord) any {
if agnetRuntimeModeForRecord(record) != agnetRuntimeModeSwarm {
func agentRuntimeOrchestrationPlanPayload(record agentDeploymentRecord) any {
if agentRuntimeModeForRecord(record) != agentRuntimeModeSwarm {
return record.Plan
}
plan := record.Plan
@@ -391,50 +392,50 @@ func agnetRuntimeOrchestrationPlanPayload(record agnetDeploymentRecord) any {
"objective": plan.Objective,
"sub_mode": firstNonEmpty(plan.SubMode, "goal_driven_swarm"),
"risk_level": plan.RiskLevel,
"budget": agnetRuntimeBudgetPayload(plan.Budget),
"budget": agentRuntimeBudgetPayload(plan.Budget),
"user_context": plan.UserContext,
"billing_context": plan.BillingContext,
"agile_context": plan.AgileContext,
"agents": agnetRuntimeRequestSwarmAgents(plan),
"resource_grants": agnetRuntimeRequestResourceGrants(plan),
"agents": agentRuntimeRequestSwarmAgents(plan),
"resource_grants": agentRuntimeRequestResourceGrants(plan),
"constraints": plan.Constraints,
"metadata": agnetRuntimeRequestMetadata(record, "orchestration_plan"),
"metadata": agentRuntimeRequestMetadata(record, "orchestration_plan"),
"agent_runtime": plan.AgentRuntime,
"acceptance": plan.AgileContext.AcceptanceCriteria,
"acceptance_tests": plan.AgileContext.AcceptanceCriteria,
}
}
func agnetRuntimeCreatePayload(record agnetDeploymentRecord, source string) gin.H {
func agentRuntimeCreatePayload(record agentDeploymentRecord, source string) gin.H {
callback := gin.H{
"url": agnetRuntimeCallbackURL(),
"subscribed_events": agnetRuntimeSubscribedEvents(),
"url": agentRuntimeCallbackURL(),
"subscribed_events": agentRuntimeSubscribedEvents(),
}
if ref := agnetRuntimeCallbackSigningSecretRef(); ref != "" {
if ref := agentRuntimeCallbackSigningSecretRef(); ref != "" {
callback["signing_secret_ref"] = ref
}
return gin.H{
"orchestration_plan": agnetRuntimeOrchestrationPlanPayload(record),
"agents": agnetRuntimeRequestAgents(record.Plan),
"orchestration_plan": agentRuntimeOrchestrationPlanPayload(record),
"agents": agentRuntimeRequestAgents(record.Plan),
"risk_level": record.Plan.RiskLevel,
"budget": agnetRuntimeBudgetPayload(record.Plan.Budget),
"budget": agentRuntimeBudgetPayload(record.Plan.Budget),
"billing_context": record.Plan.BillingContext,
"resource_grants": agnetRuntimeRequestResourceGrants(record.Plan),
"resource_grants": agentRuntimeRequestResourceGrants(record.Plan),
"callback": callback,
"agile_context": record.Plan.AgileContext,
"sub_mode": record.Plan.SubMode,
"metadata": agnetRuntimeRequestMetadata(record, source),
"metadata": agentRuntimeRequestMetadata(record, source),
}
}
func extractAgnetRuntimeData(payload map[string]any) map[string]any {
func extractAgentRuntimeData(payload map[string]any) map[string]any {
if data, ok := payload["data"].(map[string]any); ok {
return data
}
return payload
}
func agnetRuntimeEnvelopeError(payload map[string]any) string {
func agentRuntimeEnvelopeError(payload map[string]any) string {
success, hasSuccess := payload["success"].(bool)
if !hasSuccess || success {
return ""
@@ -463,44 +464,44 @@ func stringFromMap(values map[string]any, keys ...string) string {
return ""
}
func callAgnetRuntimeCreate(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, source string) (agnetRuntimeSyncResult, error) {
endpoint, err := agnetRuntimeURL(cfg.BaseURL, cfg.CreatePath)
func callAgentRuntimeCreate(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, source string) (agentRuntimeSyncResult, error) {
endpoint, err := agentRuntimeURL(cfg.BaseURL, cfg.CreatePath)
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
payload, err := common.Marshal(agnetRuntimeCreatePayload(record, source))
payload, err := common.Marshal(agentRuntimeCreatePayload(record, source))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
defer resp.Body.Close()
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if readErr != nil {
return agnetRuntimeSyncResult{}, readErr
return agentRuntimeSyncResult{}, readErr
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime create returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime create returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var envelope map[string]any
if len(body) > 0 {
if err := common.Unmarshal(body, &envelope); err != nil {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
}
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
}
data := extractAgnetRuntimeData(envelope)
result := agnetRuntimeSyncResult{
data := extractAgentRuntimeData(envelope)
result := agentRuntimeSyncResult{
RuntimeDeploymentID: stringFromMap(data, "runtime_deployment_id", "deployment_id", "id"),
RuntimeSwarmID: stringFromMap(data, "swarm_id", "runtime_swarm_id"),
RuntimeStatus: stringFromMap(data, "runtime_status", "status"),
@@ -509,16 +510,18 @@ func callAgnetRuntimeCreate(ctx context.Context, cfg agnetRuntimeConfig, record
return result, nil
}
func agnetRuntimeStopPath(cfg agnetRuntimeConfig, runtimeDeploymentID string) string {
func agentRuntimeStopPath(cfg agentRuntimeConfig, runtimeDeploymentID string) string {
path := strings.TrimSpace(cfg.StopPath)
if path == "" {
path = "/api/agnet/deployments/{deployment_id}/stop"
path = "/api/agent/deployments/{deployment_id}/stop"
}
return strings.ReplaceAll(path, "{deployment_id}", url.PathEscape(runtimeDeploymentID))
}
func agnetRuntimeStopPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord) string {
path := agnetRuntimeStopPath(cfg, strings.TrimSpace(record.RuntimeDeploymentID))
func agentRuntimeStopPathForRecord(cfg agentRuntimeConfig, record agentDeploymentRecord) string {
// {deployment_id} falls back to the swarm id so sub-agile-style paths still
// resolve for records that only persisted a runtime swarm id.
path := agentRuntimeStopPath(cfg, firstNonEmpty(record.RuntimeDeploymentID, record.RuntimeSwarmID))
replacer := strings.NewReplacer(
"{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
"{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
@@ -528,7 +531,7 @@ func agnetRuntimeStopPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymen
return replacer.Replace(path)
}
func agnetRuntimeStatusPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord) string {
func agentRuntimeStatusPathForRecord(cfg agentRuntimeConfig, record agentDeploymentRecord) string {
path := strings.TrimSpace(cfg.StatusPath)
if path == "" {
path = "/api/swarms/{swarm_id}/status"
@@ -543,7 +546,7 @@ func agnetRuntimeStatusPathForRecord(cfg agnetRuntimeConfig, record agnetDeploym
return replacer.Replace(path)
}
func agnetRuntimeArtifactContentPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) string {
func agentRuntimeArtifactContentPathForRecord(cfg agentRuntimeConfig, record agentDeploymentRecord, artifactID string) string {
path := strings.TrimSpace(cfg.ArtifactContentPath)
if path == "" {
path = "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"
@@ -559,11 +562,11 @@ func agnetRuntimeArtifactContentPathForRecord(cfg agnetRuntimeConfig, record agn
return replacer.Replace(path)
}
func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord) (map[string]any, int, error) {
func callAgentRuntimeStatus(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord) (map[string]any, int, error) {
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return nil, 0, errors.New("runtime identifiers missing")
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStatusPathForRecord(cfg, record))
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeStatusPathForRecord(cfg, record))
if err != nil {
return nil, 0, err
}
@@ -571,7 +574,7 @@ func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record
if err != nil {
return nil, 0, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
@@ -591,20 +594,20 @@ func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record
return nil, resp.StatusCode, err
}
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return nil, resp.StatusCode, errors.New(message)
}
return extractAgnetRuntimeData(envelope), resp.StatusCode, nil
return extractAgentRuntimeData(envelope), resp.StatusCode, nil
}
func callAgnetRuntimeArtifactContent(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) (*http.Response, error) {
func callAgentRuntimeArtifactContent(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, artifactID string) (*http.Response, error) {
if strings.TrimSpace(artifactID) == "" {
return nil, errors.New("artifact_id is required")
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return nil, errors.New("runtime identifiers missing")
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeArtifactContentPathForRecord(cfg, record, artifactID))
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeArtifactContentPathForRecord(cfg, record, artifactID))
if err != nil {
return nil, err
}
@@ -612,7 +615,7 @@ func callAgnetRuntimeArtifactContent(ctx context.Context, cfg agnetRuntimeConfig
if err != nil {
return nil, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
req.Header.Set("Accept", "*/*")
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
@@ -719,6 +722,14 @@ func anyPositiveFileSignal(values map[string]any) bool {
// artifact_type="document" (its no-files fallback) under a uri scheme that the
// old "/artifacts/summary" heuristic no longer matched.
func artifactIsSummaryOnly(artifact gin.H) bool {
// Runtime marks fallback artifacts (no real agent output) with
// metadata.synthesized=true — the authoritative non-deliverable signal
// (agent_management Sub Mode Runtime §7.2).
if meta, ok := artifact["metadata"].(map[string]any); ok {
if synth, ok := meta["synthesized"].(bool); ok && synth {
return true
}
}
atype := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["artifact_type"])))
if deliverableArtifactTypes[atype] || artifactHasFileChanges(artifact) {
return false
@@ -749,7 +760,61 @@ func runtimeArtifactsAreSummaryOnly(artifacts []gin.H) bool {
return true
}
func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]any, httpStatus int, source string) agnetRuntimeDiagnostics {
// persistedArtifactToGin adapts a stored artifact to the gin.H shape consumed
// by artifactIsSummaryOnly.
func persistedArtifactToGin(a model.AgentArtifact) gin.H {
g := gin.H{
"artifact_id": a.ArtifactID,
"artifact_type": a.ArtifactType,
"title": a.Title,
"summary": a.Summary,
"uri": a.URI,
}
if strings.TrimSpace(a.MetadataJSON) != "" {
var meta map[string]any
if err := common.UnmarshalJsonStr(a.MetadataJSON, &meta); err == nil && len(meta) > 0 {
g["metadata"] = meta
}
}
return g
}
// agentDeploymentDisplayStatus is the single status the client should show.
// Manager is the sole judge (unified spec §10.6): a `completed` runtime status
// is only surfaced as `completed` when there is a real (non-summary)
// deliverable; otherwise it is downgraded so an empty result is not shown as
// success — `needs_codegen` when only a plan/summary exists, or
// `completed_without_deliverable` when no artifact exists at all.
func agentDeploymentDisplayStatus(record agentDeploymentRecord) string {
if strings.ToLower(strings.TrimSpace(record.Status)) != "completed" {
return record.Status
}
artifacts, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
if err != nil {
common.SysLog("agentDeploymentDisplayStatus: " + err.Error())
return record.Status
}
for _, a := range artifacts {
if !artifactIsSummaryOnly(persistedArtifactToGin(a)) {
return "completed"
}
}
if len(artifacts) > 0 {
return "needs_codegen"
}
return "completed_without_deliverable"
}
// withDisplayStatus returns the record with DisplayStatus computed for response.
func withDisplayStatus(record agentDeploymentRecord) agentDeploymentRecord {
record.DisplayStatus = agentDeploymentDisplayStatus(record)
return record
}
func buildAgentRuntimeDiagnostics(record agentDeploymentRecord, data map[string]any, httpStatus int, source string) agentRuntimeDiagnostics {
agents := mapSliceFromAny(data["agents"])
artifacts := mapSliceFromAny(data["artifacts"])
status := stringFromMap(data, "runtime_status", "status")
@@ -771,9 +836,9 @@ func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]
warnings = append(warnings, "runtime_zero_model_usage")
}
}
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: agnetRuntimeModeForRecord(record),
RuntimeMode: agentRuntimeModeForRecord(record),
SubMode: record.SubMode,
RuntimeDeploymentID: record.RuntimeDeploymentID,
RuntimeSwarmID: record.RuntimeSwarmID,
@@ -787,36 +852,36 @@ func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]
Artifacts: artifacts,
Metrics: metrics,
Warnings: warnings,
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
func agnetRuntimeDiagnosticsForRecord(ctx context.Context, record agnetDeploymentRecord) agnetRuntimeDiagnostics {
mode := agnetRuntimeModeForRecord(record)
cfg := agnetRuntimeClientConfigForMode(mode)
func agentRuntimeDiagnosticsForRecord(ctx context.Context, record agentDeploymentRecord) agentRuntimeDiagnostics {
mode := agentRuntimeModeForRecord(record)
cfg := agentRuntimeClientConfigForMode(mode)
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
DataSource: "not_configured",
Warnings: []string{"runtime_not_configured"},
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
DataSource: "missing_runtime_id",
Warnings: []string{"runtime_identifiers_missing"},
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
data, status, err := callAgnetRuntimeStatus(ctx, cfg, record)
data, status, err := callAgentRuntimeStatus(ctx, cfg, record)
if err != nil {
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
@@ -824,25 +889,25 @@ func agnetRuntimeDiagnosticsForRecord(ctx context.Context, record agnetDeploymen
RuntimeSwarmID: record.RuntimeSwarmID,
DataSource: "runtime_status_error",
HTTPStatus: status,
ErrorMessage: truncateAgnetFailureReason(err.Error()),
ErrorMessage: truncateAgentFailureReason(err.Error()),
Warnings: []string{"runtime_status_query_failed"},
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
return buildAgnetRuntimeDiagnostics(record, data, status, "runtime_status")
return buildAgentRuntimeDiagnostics(record, data, status, "runtime_status")
}
func AgnetGetUserDeploymentRuntimeDiagnostics(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentGetUserDeploymentRuntimeDiagnostics(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record)).Timeout)
ctx, cancel := context.WithTimeout(c.Request.Context(), agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record)).Timeout)
defer cancel()
common.ApiSuccess(c, agnetRuntimeDiagnosticsForRecord(ctx, record))
common.ApiSuccess(c, agentRuntimeDiagnosticsForRecord(ctx, record))
}
func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploymentRecord, approvalID string) string {
func agentRuntimeApprovalDecisionPath(cfg agentRuntimeConfig, record agentDeploymentRecord, approvalID string) string {
path := strings.TrimSpace(cfg.ApprovalDecisionPath)
if path == "" {
path = "/api/swarms/{swarm_id}/approvals/{approval_id}"
@@ -858,8 +923,8 @@ func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploy
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))
func callAgentRuntimeApprovalDecision(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, approval model.AgentApprovalRequest, lease *model.AgentCredentialLease, decision string) error {
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeApprovalDecisionPath(cfg, record, approval.ApprovalID))
if err != nil {
return err
}
@@ -922,87 +987,87 @@ func callAgnetRuntimeApprovalDecision(ctx context.Context, cfg agnetRuntimeConfi
if err := common.Unmarshal(respBody, &envelope); err != nil {
return err
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return errors.New(message)
}
return nil
}
func syncAgnetRuntimeApprovalDecision(c *gin.Context, approval *model.AgnetApprovalRequest, lease *model.AgnetCredentialLease, decision string) {
func syncAgentRuntimeApprovalDecision(c *gin.Context, approval *model.AgentApprovalRequest, lease *model.AgentCredentialLease, decision string) {
if approval == nil || strings.TrimSpace(approval.DeploymentID) == "" {
return
}
if !agnetRuntimeClientConfigForMode(agnetRuntimeModeAgnet).Enabled && !agnetRuntimeClientConfigForMode(agnetRuntimeModeSwarm).Enabled {
if !agentRuntimeClientConfigForMode(agentRuntimeModeAgent).Enabled && !agentRuntimeClientConfigForMode(agentRuntimeModeSwarm).Enabled {
return
}
record, ok := findAgnetDeploymentRecord(approval.DeploymentID)
record, ok := findAgentDeploymentRecord(approval.DeploymentID)
if !ok {
recordAgnetApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "deployment not found")
recordAgentApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "deployment not found")
return
}
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if !cfg.Enabled {
return
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
recordAgnetApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "runtime identifiers missing")
recordAgentApprovalAudit("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())
if err := callAgentRuntimeApprovalDecision(ctx, cfg, record, *approval, lease, decision); err != nil {
recordAgentApprovalAudit("runtime.approval_decision.failed", approval, lease, "failed", truncateAgentFailureReason(err.Error()))
common.SysLog("Agent runtime approval decision failed for " + approval.ApprovalID + ": " + err.Error())
return
}
recordAgnetApprovalAudit("runtime.approval_decision.accepted", approval, lease, "ok", "")
recordAgentApprovalAudit("runtime.approval_decision.accepted", approval, lease, "ok", "")
}
func callAgnetRuntimeStop(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, reason string) (agnetRuntimeSyncResult, error) {
func callAgentRuntimeStop(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, reason string) (agentRuntimeSyncResult, error) {
runtimeDeploymentID := strings.TrimSpace(record.RuntimeDeploymentID)
if runtimeDeploymentID == "" {
return agnetRuntimeSyncResult{}, nil
return agentRuntimeSyncResult{}, nil
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStopPathForRecord(cfg, record))
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeStopPathForRecord(cfg, record))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
payload, err := common.Marshal(gin.H{
"reason": firstNonEmpty(strings.TrimSpace(reason), "Heicode Manager requested stop"),
"manager_deployment_id": record.DeploymentID,
})
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
defer resp.Body.Close()
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if readErr != nil {
return agnetRuntimeSyncResult{}, readErr
return agentRuntimeSyncResult{}, readErr
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime stop returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime stop returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var envelope map[string]any
if len(body) > 0 {
if err := common.Unmarshal(body, &envelope); err != nil {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
}
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
}
data := extractAgnetRuntimeData(envelope)
return agnetRuntimeSyncResult{
data := extractAgentRuntimeData(envelope)
return agentRuntimeSyncResult{
RuntimeDeploymentID: firstNonEmpty(stringFromMap(data, "runtime_deployment_id", "deployment_id", "id"), runtimeDeploymentID),
RuntimeSwarmID: stringFromMap(data, "swarm_id", "runtime_swarm_id"),
RuntimeStatus: stringFromMap(data, "runtime_status", "status"),
@@ -1010,25 +1075,25 @@ func callAgnetRuntimeStop(ctx context.Context, cfg agnetRuntimeConfig, record ag
}, nil
}
func syncAgnetRuntimeStop(c *gin.Context, record agnetDeploymentRecord, reason string) (agnetDeploymentRecord, bool) {
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
func syncAgentRuntimeStop(c *gin.Context, record agentDeploymentRecord, reason string) (agentDeploymentRecord, bool) {
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if !cfg.Enabled || strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return record, true
}
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
result, err := callAgnetRuntimeStop(ctx, cfg, record, reason)
record.RuntimeLastSyncAt = agnetNow()
result, err := callAgentRuntimeStop(ctx, cfg, record, reason)
record.RuntimeLastSyncAt = agentNow()
if err != nil {
record.RuntimeState = agnetRuntimeStateFailed
record.FailureReason = truncateAgnetFailureReason(err.Error())
record.UpdatedAt = agnetNow()
_ = updateAgnetDeploymentRecord(record)
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
recordAgnetRuntimeSyncAudit(record, "runtime.stop.failed", "failed")
agnetError(c, "RUNTIME_STOP_FAILED", record.FailureReason)
record.RuntimeState = agentRuntimeStateFailed
record.FailureReason = truncateAgentFailureReason(err.Error())
record.UpdatedAt = agentNow()
_ = updateAgentDeploymentRecord(record)
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
recordAgentRuntimeSyncAudit(record, "runtime.stop.failed", "failed")
agentError(c, "RUNTIME_STOP_FAILED", record.FailureReason)
return record, false
}
record.RuntimeState = firstNonEmpty(result.RuntimeStatus, "stopped")
@@ -1039,33 +1104,33 @@ func syncAgnetRuntimeStop(c *gin.Context, record agnetDeploymentRecord, reason s
record.RuntimeSwarmID = result.RuntimeSwarmID
}
record.FailureReason = ""
recordAgnetRuntimeSyncAudit(record, "runtime.stop.accepted", "ok")
recordAgentRuntimeSyncAudit(record, "runtime.stop.accepted", "ok")
return record, true
}
func updateAgnetRuntimeSyncState(record agnetDeploymentRecord, result agnetRuntimeSyncResult, syncErr error) agnetDeploymentRecord {
record.RuntimeLastSyncAt = agnetNow()
func updateAgentRuntimeSyncState(record agentDeploymentRecord, result agentRuntimeSyncResult, syncErr error) agentDeploymentRecord {
record.RuntimeLastSyncAt = agentNow()
if syncErr != nil {
record.RuntimeState = agnetRuntimeStateFailed
record.FailureReason = truncateAgnetFailureReason(syncErr.Error())
record.RuntimeState = agentRuntimeStateFailed
record.FailureReason = truncateAgentFailureReason(syncErr.Error())
} else {
record.RuntimeState = firstNonEmpty(result.RuntimeStatus, agnetRuntimeStateSynced)
record.RuntimeState = firstNonEmpty(result.RuntimeStatus, agentRuntimeStateSynced)
record.RuntimeDeploymentID = result.RuntimeDeploymentID
record.RuntimeSwarmID = result.RuntimeSwarmID
record.FailureReason = ""
}
record.UpdatedAt = agnetNow()
if err := updateAgnetDeploymentRecord(record); err != nil {
common.SysLog("updateAgnetRuntimeSyncState: " + err.Error())
record.UpdatedAt = agentNow()
if err := updateAgentDeploymentRecord(record); err != nil {
common.SysLog("updateAgentRuntimeSyncState: " + err.Error())
}
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
return record
}
func recordAgnetRuntimeSyncAudit(record agnetDeploymentRecord, event string, result string) {
recordAgnetAuditEvent(agnetEvent{
func recordAgentRuntimeSyncAudit(record agentDeploymentRecord, event string, result string) {
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: event,
SchemaVersion: 1,
@@ -1074,79 +1139,79 @@ func recordAgnetRuntimeSyncAudit(record agnetDeploymentRecord, event string, res
BindingScope: firstPlanBindingScope(record.Plan),
DeploymentID: record.DeploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: agnetNow(),
}, "agnet_runtime_bridge", record.DeploymentID, "", result)
OccurredAt: agentNow(),
}, "agent_runtime_bridge", record.DeploymentID, "", result)
}
func dispatchAgnetRuntimeCreate(record agnetDeploymentRecord, source string, cfg agnetRuntimeConfig) agnetDeploymentRecord {
recordAgnetRuntimeSyncAudit(record, "runtime.sync.started", "started")
func dispatchAgentRuntimeCreate(record agentDeploymentRecord, source string, cfg agentRuntimeConfig) agentDeploymentRecord {
recordAgentRuntimeSyncAudit(record, "runtime.sync.started", "started")
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
result, err := callAgnetRuntimeCreate(ctx, cfg, record, source)
record = updateAgnetRuntimeSyncState(record, result, err)
result, err := callAgentRuntimeCreate(ctx, cfg, record, source)
record = updateAgentRuntimeSyncState(record, result, err)
if err != nil {
common.SysLog("Agnet runtime shadow create failed for " + record.DeploymentID + ": " + err.Error())
recordAgnetRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
common.SysLog("Agent runtime shadow create failed for " + record.DeploymentID + ": " + err.Error())
recordAgentRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
return record
}
recordAgnetRuntimeSyncAudit(record, "runtime.sync.accepted", "ok")
recordAgentRuntimeSyncAudit(record, "runtime.sync.accepted", "ok")
return record
}
func maybeDispatchAgnetRuntimeCreate(c *gin.Context, record agnetDeploymentRecord, source string) agnetDeploymentRecord {
mode := agnetRuntimeModeForSource(source)
func maybeDispatchAgentRuntimeCreate(c *gin.Context, record agentDeploymentRecord, source string) agentDeploymentRecord {
mode := agentRuntimeModeForSource(source)
if strings.TrimSpace(record.Plan.Metadata.RuntimeMode) == "" {
record.Plan.Metadata.RuntimeMode = mode
} else {
mode = agnetRuntimeModeForRecord(record)
mode = agentRuntimeModeForRecord(record)
}
cfg := agnetRuntimeClientConfigForMode(mode)
cfg := agentRuntimeClientConfigForMode(mode)
if !cfg.Enabled {
return record
}
if _, err := agnetRuntimeURL(cfg.BaseURL, cfg.CreatePath); err != nil {
record.RuntimeState = agnetRuntimeStateFailed
record.RuntimeLastSyncAt = agnetNow()
record.FailureReason = truncateAgnetFailureReason(err.Error())
record.UpdatedAt = agnetNow()
_ = updateAgnetDeploymentRecord(record)
recordAgnetRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
if _, err := agentRuntimeURL(cfg.BaseURL, cfg.CreatePath); err != nil {
record.RuntimeState = agentRuntimeStateFailed
record.RuntimeLastSyncAt = agentNow()
record.FailureReason = truncateAgentFailureReason(err.Error())
record.UpdatedAt = agentNow()
_ = updateAgentDeploymentRecord(record)
recordAgentRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
return record
}
record.RuntimeState = agnetRuntimeStateSyncing
record.RuntimeLastSyncAt = agnetNow()
record.UpdatedAt = agnetNow()
if err := updateAgnetDeploymentRecord(record); err != nil {
common.SysLog("maybeDispatchAgnetRuntimeCreate: " + err.Error())
record.RuntimeState = agentRuntimeStateSyncing
record.RuntimeLastSyncAt = agentNow()
record.UpdatedAt = agentNow()
if err := updateAgentDeploymentRecord(record); err != nil {
common.SysLog("maybeDispatchAgentRuntimeCreate: " + err.Error())
}
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
if cfg.Async {
sourceCopy := source
recordCopy := record
go dispatchAgnetRuntimeCreate(recordCopy, sourceCopy, cfg)
go dispatchAgentRuntimeCreate(recordCopy, sourceCopy, cfg)
return record
}
return dispatchAgnetRuntimeCreate(record, source, cfg)
return dispatchAgentRuntimeCreate(record, source, cfg)
}
func AgnetRuntimeHealth(c *gin.Context) {
cfg := agnetRuntimeClientConfigForMode(c.Query("mode"))
func AgentRuntimeHealth(c *gin.Context) {
cfg := agentRuntimeClientConfigForMode(c.Query("mode"))
data := gin.H{
"enabled": cfg.Enabled,
"configured": cfg.BaseURL != "",
"create_path": cfg.CreatePath,
"health_path": cfg.HealthPath,
"stop_path": cfg.StopPath,
"mode": normalizeAgnetRuntimeMode(c.Query("mode")),
"mode": normalizeAgentRuntimeMode(c.Query("mode")),
}
if cfg.BaseURL == "" {
data["status"] = "not_configured"
common.ApiSuccess(c, data)
return
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, cfg.HealthPath)
endpoint, err := agentRuntimeURL(cfg.BaseURL, cfg.HealthPath)
if err != nil {
data["status"] = "invalid_config"
data["message"] = err.Error()
+299
View File
@@ -0,0 +1,299 @@
package controller
import (
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
type agentTaskDeploymentDraftRequest struct {
Task agentTaskSnapshot `json:"task"`
SubMode string `json:"sub_mode"`
RiskLevel string `json:"risk_level"`
Budget agentBudget `json:"budget"`
BindingScope string `json:"binding_scope"`
RoleTemplates []string `json:"role_templates"`
DefaultModelID string `json:"default_model_id"`
RoleModels map[string]string `json:"role_models"`
ResourceGrants []agentResourceGrant `json:"resource_grants"`
}
type agentTaskSnapshot struct {
ID string `json:"id"`
Name string `json:"name"`
Intent string `json:"intent"`
Status string `json:"status"`
Card map[string]any `json:"card"`
}
func stringFromTaskCard(card map[string]any, key string) string {
if card == nil {
return ""
}
if value, ok := card[key].(string); ok {
return strings.TrimSpace(value)
}
return ""
}
func objectiveFromTaskSnapshot(task agentTaskSnapshot) string {
for _, value := range []string{
stringFromTaskCard(task.Card, "goal"),
task.Name,
task.Intent,
} {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func normalizeAgentDraftBudget(budget agentBudget) agentBudget {
if budget.MaxTokens <= 0 {
budget.MaxTokens = 120000
}
if budget.MaxCostUSD <= 0 {
budget.MaxCostUSD = 8
}
if budget.MaxDurationSec <= 0 {
budget.MaxDurationSec = 3600
}
return budget
}
func normalizeAgentDraftRoleTemplates(values []string) []string {
roles := make([]string, 0, len(values))
for _, value := range values {
role := strings.TrimSpace(value)
if role == "" {
continue
}
roles = append(roles, role)
}
if len(roles) == 0 {
return []string{"backend"}
}
return roles
}
func defaultAgentTaskBindingScope(taskID string) string {
bindingScope := "task-" + sanitizeAgentRef(taskID)
if bindingScope == "task-" {
return "task-local"
}
return bindingScope
}
func defaultTaskDraftResourceGrant(userID string, bindingScope string, role string, taskID string) agentResourceGrant {
return agentResourceGrant{
GrantID: "grant-" + sanitizeAgentRef(taskID) + "-" + sanitizeAgentRef(role),
ResourceID: "task-" + sanitizeAgentRef(taskID) + "-context",
ResourceType: agentResourceProjectDoc,
UserID: userID,
BindingScope: bindingScope,
TargetRole: role,
TargetAgentRef: "agent-" + sanitizeAgentRef(role) + "-1",
PermissionScope: []string{"doc:read"},
Constraints: map[string]string{"ref": "task-card"},
Metadata: map[string]string{"provider": "heicode-task", "resource_ref": taskID},
Status: agentGrantStatusActive,
Audit: map[string]string{"source": "heicode-task-draft"},
}
}
func sanitizeAgentRef(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
var b strings.Builder
for _, r := range value {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '-' || r == '_':
b.WriteRune(r)
default:
b.WriteRune('-')
}
}
return strings.Trim(b.String(), "-")
}
func buildAgentDraftAgentPlan(role string, defaultModelID string, grants []agentResourceGrant) agentAgentPlan {
if defaultModelID == "" {
defaultModelID = defaultAgentModelID()
}
return agentAgentPlan{
RoleTemplate: role,
Goal: fmt.Sprintf("Execute the Heicode task as %s within the approved resource scope.", role),
DefaultModelID: defaultModelID,
ResourceGrants: grants,
}
}
func normalizeTaskDraftResourceGrants(userID string, bindingScope string, role string, taskID string, grants []agentResourceGrant) []agentResourceGrant {
if len(grants) == 0 {
return []agentResourceGrant{defaultTaskDraftResourceGrant(userID, bindingScope, role, taskID)}
}
normalized := make([]agentResourceGrant, 0, len(grants))
for idx, grant := range grants {
grant.UserID = userID
if strings.TrimSpace(grant.GrantID) == "" {
grant.GrantID = fmt.Sprintf("grant-%s-%s-%d", sanitizeAgentRef(taskID), sanitizeAgentRef(role), idx+1)
}
if strings.TrimSpace(grant.BindingScope) == "" {
grant.BindingScope = bindingScope
}
if strings.TrimSpace(grant.TargetRole) == "" {
grant.TargetRole = role
}
if strings.TrimSpace(grant.TargetAgentRef) == "" {
grant.TargetAgentRef = "agent-" + sanitizeAgentRef(role) + "-1"
}
if strings.TrimSpace(grant.Status) == "" {
grant.Status = agentGrantStatusActive
}
grant = resolveResourceBindingIntoGrant(userID, grant)
normalized = append(normalized, grant)
}
return normalized
}
// resolveResourceBindingIntoGrant injects the stored ResourceBinding's real
// secret_ref and resource metadata when the client referenced a binding by id
// instead of inlining a secret_ref (unified spec §17.6). The binding must be
// owned by the requesting user; unknown/unowned ids are left untouched so the
// existing plan validation surfaces a clear error.
func resolveResourceBindingIntoGrant(userID string, grant agentResourceGrant) agentResourceGrant {
if grant.ResourceBindingID <= 0 || model.DB == nil {
return grant
}
uid, _ := strconv.Atoi(strings.TrimSpace(userID))
if uid <= 0 {
return grant
}
var binding model.ResourceBinding
if err := model.DB.Where("id = ? AND user_id = ?", grant.ResourceBindingID, uid).First(&binding).Error; err != nil {
return grant
}
if strings.TrimSpace(grant.SecretRef) == "" {
grant.SecretRef = strings.TrimSpace(binding.SecretRef)
}
if strings.TrimSpace(grant.ResourceID) == "" {
grant.ResourceID = fmt.Sprintf("rb_%d", binding.Id)
}
if strings.TrimSpace(grant.ResourceType) == "" {
grant.ResourceType = strings.TrimSpace(binding.ResourceType)
}
if strings.TrimSpace(grant.BindingScope) == "" {
grant.BindingScope = strings.TrimSpace(binding.BindingScope)
}
return grant
}
func AgentCreateTaskDeploymentDraft(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
agentError(c, "TASK_NOT_FOUND", "task_id is required")
return
}
var req agentTaskDeploymentDraftRequest
if err := c.ShouldBindJSON(&req); err != nil {
agentError(c, "POLICY_REJECTED", err.Error())
return
}
if strings.TrimSpace(req.Task.ID) == "" {
agentError(c, "TASK_NOT_FOUND", "task snapshot is required")
return
}
if strings.TrimSpace(req.Task.ID) != taskID {
agentError(c, "TASK_CONFLICT", "task snapshot id must match route task_id")
return
}
if !isValidAgentSubMode(req.SubMode) {
agentError(c, "POLICY_REJECTED", "sub_mode must be agile or waterfall")
return
}
userID := strconv.Itoa(c.GetInt("id"))
if userID == "0" {
agentError(c, "POLICY_REJECTED", "authenticated user is required")
return
}
objective := objectiveFromTaskSnapshot(req.Task)
if objective == "" {
agentError(c, "POLICY_REJECTED", "task objective is required")
return
}
bindingScope := strings.TrimSpace(req.BindingScope)
if bindingScope == "" {
bindingScope = defaultAgentTaskBindingScope(taskID)
}
roles := normalizeAgentDraftRoleTemplates(req.RoleTemplates)
riskLevel := strings.TrimSpace(req.RiskLevel)
if riskLevel == "" {
riskLevel = agentRiskLow
}
defaultModelID := strings.TrimSpace(req.DefaultModelID)
group := strings.TrimSpace(c.GetString("group"))
agents := make([]agentAgentPlan, 0, len(roles))
runtimeAgents := make([]agentRuntimeAgent, 0, len(roles))
// per_role model selection (unified spec §9): role_models[role] wins, then
// the request default, then the platform default. Every resolved model is
// collected into allowed_model_ids so create-time validation accepts them.
allowedSeen := map[string]bool{}
allowedModels := []string{}
addAllowedModel := func(m string) {
m = strings.TrimSpace(m)
if m == "" || allowedSeen[m] {
return
}
allowedSeen[m] = true
allowedModels = append(allowedModels, m)
}
for _, role := range roles {
grants := normalizeTaskDraftResourceGrants(userID, bindingScope, role, taskID, req.ResourceGrants)
modelRef := firstNonEmpty(req.RoleModels[role], defaultModelID, defaultAgentModelID())
agents = append(agents, buildAgentDraftAgentPlan(role, modelRef, grants))
runtimeAgents = append(runtimeAgents, agentRuntimeAgent{Role: role, ModelRef: modelRef, InstanceCount: 1})
addAllowedModel(modelRef)
}
plan := agentOrchestrationPlan{
IntentID: taskID,
TemplateHint: "heicode-task",
Objective: objective,
SubMode: normalizeAgentSubMode(req.SubMode),
RiskLevel: riskLevel,
Budget: normalizeAgentDraftBudget(req.Budget),
UserContext: agentUserContext{
UserID: userID,
Role: "user",
ChannelID: group,
},
AgentRuntime: agentAgentRuntime{Platform: "agent", Agents: runtimeAgents},
Agents: agents,
Constraints: agentConstraints{AllowedModelIDs: allowedModels},
Metadata: agentMetadata{
CorrelationID: "task-" + sanitizeAgentRef(taskID) + "-" + common.GetUUID()[:8],
},
}
if group != "" {
plan.BillingContext = agentBillingContext{Provider: "newapi", NewAPIGroup: group}
}
if !validateOrchestrationPlan(c, plan) {
return
}
common.ApiSuccess(c, gin.H{
"task_id": taskID,
"orchestration_plan": plan,
})
}
-253
View File
@@ -1,253 +0,0 @@
package controller
import (
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
)
type agnetTaskDeploymentDraftRequest struct {
Task agnetTaskSnapshot `json:"task"`
SubMode string `json:"sub_mode"`
RiskLevel string `json:"risk_level"`
Budget agnetBudget `json:"budget"`
BindingScope string `json:"binding_scope"`
RoleTemplates []string `json:"role_templates"`
DefaultModelID string `json:"default_model_id"`
ResourceGrants []agnetResourceGrant `json:"resource_grants"`
}
type agnetTaskSnapshot struct {
ID string `json:"id"`
Name string `json:"name"`
Intent string `json:"intent"`
Status string `json:"status"`
Card map[string]any `json:"card"`
}
func stringFromTaskCard(card map[string]any, key string) string {
if card == nil {
return ""
}
if value, ok := card[key].(string); ok {
return strings.TrimSpace(value)
}
return ""
}
func objectiveFromTaskSnapshot(task agnetTaskSnapshot) string {
for _, value := range []string{
stringFromTaskCard(task.Card, "goal"),
task.Name,
task.Intent,
} {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func normalizeAgnetDraftBudget(budget agnetBudget) agnetBudget {
if budget.MaxTokens <= 0 {
budget.MaxTokens = 120000
}
if budget.MaxCostUSD <= 0 {
budget.MaxCostUSD = 8
}
if budget.MaxDurationSec <= 0 {
budget.MaxDurationSec = 3600
}
return budget
}
func normalizeAgnetDraftRoleTemplates(values []string) []string {
roles := make([]string, 0, len(values))
for _, value := range values {
role := strings.TrimSpace(value)
if role == "" {
continue
}
roles = append(roles, role)
}
if len(roles) == 0 {
return []string{"backend"}
}
return roles
}
func defaultAgnetTaskBindingScope(taskID string) string {
bindingScope := "task-" + sanitizeAgnetRef(taskID)
if bindingScope == "task-" {
return "task-local"
}
return bindingScope
}
func defaultTaskDraftResourceGrant(userID string, bindingScope string, role string, taskID string) agnetResourceGrant {
return agnetResourceGrant{
GrantID: "grant-" + sanitizeAgnetRef(taskID) + "-" + sanitizeAgnetRef(role),
ResourceID: "task-" + sanitizeAgnetRef(taskID) + "-context",
ResourceType: agnetResourceProjectDoc,
UserID: userID,
BindingScope: bindingScope,
TargetRole: role,
TargetAgentRef: "agent-" + sanitizeAgnetRef(role) + "-1",
PermissionScope: []string{"doc:read"},
Constraints: map[string]string{"ref": "task-card"},
Metadata: map[string]string{"provider": "heicode-task", "resource_ref": taskID},
Status: agnetGrantStatusActive,
Audit: map[string]string{"source": "heicode-task-draft"},
}
}
func sanitizeAgnetRef(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
var b strings.Builder
for _, r := range value {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '-' || r == '_':
b.WriteRune(r)
default:
b.WriteRune('-')
}
}
return strings.Trim(b.String(), "-")
}
func buildAgnetDraftAgentPlan(role string, defaultModelID string, grants []agnetResourceGrant) agnetAgentPlan {
if defaultModelID == "" {
defaultModelID = defaultAgnetModelID()
}
return agnetAgentPlan{
RoleTemplate: role,
Goal: fmt.Sprintf("Execute the Heicode task as %s within the approved resource scope.", role),
DefaultModelID: defaultModelID,
ResourceGrants: grants,
}
}
func normalizeTaskDraftResourceGrants(userID string, bindingScope string, role string, taskID string, grants []agnetResourceGrant) []agnetResourceGrant {
if len(grants) == 0 {
return []agnetResourceGrant{defaultTaskDraftResourceGrant(userID, bindingScope, role, taskID)}
}
normalized := make([]agnetResourceGrant, 0, len(grants))
for idx, grant := range grants {
grant.UserID = userID
if strings.TrimSpace(grant.GrantID) == "" {
grant.GrantID = fmt.Sprintf("grant-%s-%s-%d", sanitizeAgnetRef(taskID), sanitizeAgnetRef(role), idx+1)
}
if strings.TrimSpace(grant.BindingScope) == "" {
grant.BindingScope = bindingScope
}
if strings.TrimSpace(grant.TargetRole) == "" {
grant.TargetRole = role
}
if strings.TrimSpace(grant.TargetAgentRef) == "" {
grant.TargetAgentRef = "agent-" + sanitizeAgnetRef(role) + "-1"
}
if strings.TrimSpace(grant.Status) == "" {
grant.Status = agnetGrantStatusActive
}
normalized = append(normalized, grant)
}
return normalized
}
func AgnetCreateTaskDeploymentDraft(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
agnetError(c, "TASK_NOT_FOUND", "task_id is required")
return
}
var req agnetTaskDeploymentDraftRequest
if err := c.ShouldBindJSON(&req); err != nil {
agnetError(c, "POLICY_REJECTED", err.Error())
return
}
if strings.TrimSpace(req.Task.ID) == "" {
agnetError(c, "TASK_NOT_FOUND", "task snapshot is required")
return
}
if strings.TrimSpace(req.Task.ID) != taskID {
agnetError(c, "TASK_CONFLICT", "task snapshot id must match route task_id")
return
}
if !isValidAgnetSubMode(req.SubMode) {
agnetError(c, "POLICY_REJECTED", "sub_mode must be agile or waterfall")
return
}
userID := strconv.Itoa(c.GetInt("id"))
if userID == "0" {
agnetError(c, "POLICY_REJECTED", "authenticated user is required")
return
}
objective := objectiveFromTaskSnapshot(req.Task)
if objective == "" {
agnetError(c, "POLICY_REJECTED", "task objective is required")
return
}
bindingScope := strings.TrimSpace(req.BindingScope)
if bindingScope == "" {
bindingScope = defaultAgnetTaskBindingScope(taskID)
}
roles := normalizeAgnetDraftRoleTemplates(req.RoleTemplates)
riskLevel := strings.TrimSpace(req.RiskLevel)
if riskLevel == "" {
riskLevel = agnetRiskLow
}
defaultModelID := strings.TrimSpace(req.DefaultModelID)
group := strings.TrimSpace(c.GetString("group"))
agents := make([]agnetAgentPlan, 0, len(roles))
runtimeAgents := make([]agnetRuntimeAgent, 0, len(roles))
for _, role := range roles {
grants := normalizeTaskDraftResourceGrants(userID, bindingScope, role, taskID, req.ResourceGrants)
agents = append(agents, buildAgnetDraftAgentPlan(role, defaultModelID, grants))
modelRef := defaultModelID
if modelRef == "" {
modelRef = defaultAgnetModelID()
}
runtimeAgents = append(runtimeAgents, agnetRuntimeAgent{Role: role, ModelRef: modelRef, InstanceCount: 1})
}
plan := agnetOrchestrationPlan{
IntentID: taskID,
TemplateHint: "heicode-task",
Objective: objective,
SubMode: normalizeAgnetSubMode(req.SubMode),
RiskLevel: riskLevel,
Budget: normalizeAgnetDraftBudget(req.Budget),
UserContext: agnetUserContext{
UserID: userID,
Role: "user",
ChannelID: group,
},
AgentRuntime: agnetAgentRuntime{Platform: "agnet", Agents: runtimeAgents},
Agents: agents,
Constraints: agnetConstraints{AllowedModelIDs: []string{}},
Metadata: agnetMetadata{
CorrelationID: "task-" + sanitizeAgnetRef(taskID) + "-" + common.GetUUID()[:8],
},
}
if group != "" {
plan.BillingContext = agnetBillingContext{Provider: "newapi", NewAPIGroup: group}
}
if !validateOrchestrationPlan(c, plan) {
return
}
common.ApiSuccess(c, gin.H{
"task_id": taskID,
"orchestration_plan": plan,
})
}
@@ -20,14 +20,14 @@ import (
"gorm.io/gorm"
)
// HeicodeAgnetSessionRequest accepts tokens obtained only from Agnet identity platform.
// HeicodeAgentSessionRequest accepts tokens obtained only from Agent identity platform.
// Manager verifies them server-side and issues the browser session cookie (same as password login).
type HeicodeAgnetSessionRequest struct {
type HeicodeAgentSessionRequest struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type agnetMeEnvelope struct {
type agentMeEnvelope struct {
Success bool `json:"success"`
Message string `json:"message"`
Detail string `json:"detail"`
@@ -41,7 +41,7 @@ type agnetMeEnvelope struct {
} `json:"data"`
}
type agnetRefreshEnvelope struct {
type agentRefreshEnvelope struct {
Success bool `json:"success"`
Detail string `json:"detail"`
Message string `json:"message"`
@@ -80,10 +80,10 @@ func parseEmailList(raw string) map[string]struct{} {
return out
}
// roleFromAgnetWithEmail decides the local role for a JIT-synced Agnet user.
// roleFromAgentWithEmail decides the local role for a JIT-synced Agent user.
//
// 安全策略:管理员权限只能通过本地配置(环境变量白名单)显式授予,
// **不信任** Agnet 平台返回的 role 字段。这样防止外部身份平台
// **不信任** Agent 平台返回的 role 字段。这样防止外部身份平台
// 的角色被直接映射到 Manager 的高权限角色。
//
// - 邮箱命中 HEICODE_ROOT_EMAILS -> RoleRootUser
@@ -92,7 +92,7 @@ func parseEmailList(raw string) map[string]struct{} {
//
// 第二参数 `role` 当前未使用,保留是为了未来扩展(例如在策略中允许
// 信任部分上游 role),不破坏调用点签名。
func roleFromAgnetWithEmail(_ string, email string) int {
func roleFromAgentWithEmail(_ string, email string) int {
emailKey := strings.ToLower(strings.TrimSpace(email))
rootEmails := parseEmailList(os.Getenv("HEICODE_ROOT_EMAILS"))
if _, ok := rootEmails[emailKey]; ok {
@@ -105,26 +105,26 @@ func roleFromAgnetWithEmail(_ string, email string) int {
return common.RoleCommonUser
}
func statusFromAgnet(status string) int {
func statusFromAgent(status string) int {
if strings.EqualFold(strings.TrimSpace(status), "active") {
return common.UserStatusEnabled
}
return common.UserStatusDisabled
}
func agnetHTTPClient() *http.Client {
func agentHTTPClient() *http.Client {
return &http.Client{Timeout: 15 * time.Second}
}
func fetchAgnetMe(baseURL, accessToken string) (agnetMeEnvelope, int, error) {
var out agnetMeEnvelope
func fetchAgentMe(baseURL, accessToken string) (agentMeEnvelope, int, error) {
var out agentMeEnvelope
req, err := http.NewRequest(http.MethodGet, baseURL+"/api/auth/me", nil)
if err != nil {
return out, 0, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-Request-Id", common.GetUUID())
res, err := agnetHTTPClient().Do(req)
res, err := agentHTTPClient().Do(req)
if err != nil {
return out, 0, err
}
@@ -134,12 +134,12 @@ func fetchAgnetMe(baseURL, accessToken string) (agnetMeEnvelope, int, error) {
return out, res.StatusCode, err
}
if err := common.Unmarshal(body, &out); err != nil {
return out, res.StatusCode, fmt.Errorf("invalid response from Agnet /me: %w", err)
return out, res.StatusCode, fmt.Errorf("invalid response from Agent /me: %w", err)
}
return out, res.StatusCode, nil
}
func fetchAgnetRefresh(baseURL, refreshToken string) (access string, refresh string, err error) {
func fetchAgentRefresh(baseURL, refreshToken string) (access string, refresh string, err error) {
req, err := http.NewRequest(http.MethodPost, baseURL+"/api/auth/refresh", nil)
if err != nil {
return "", "", err
@@ -147,7 +147,7 @@ func fetchAgnetRefresh(baseURL, refreshToken string) (access string, refresh str
req.Header.Set("Authorization", "Bearer "+refreshToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-Id", common.GetUUID())
res, err := agnetHTTPClient().Do(req)
res, err := agentHTTPClient().Do(req)
if err != nil {
return "", "", err
}
@@ -156,9 +156,9 @@ func fetchAgnetRefresh(baseURL, refreshToken string) (access string, refresh str
if err != nil {
return "", "", err
}
var env agnetRefreshEnvelope
var env agentRefreshEnvelope
if err := common.Unmarshal(body, &env); err != nil {
return "", "", fmt.Errorf("invalid response from Agnet /refresh: %w", err)
return "", "", fmt.Errorf("invalid response from Agent /refresh: %w", err)
}
if !env.Success || env.Data.Token == "" {
msg := env.Message
@@ -212,7 +212,7 @@ func markBillingProviderNewapi(email string) {
req.Header.Set("Authorization", "Bearer "+tok)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-Id", common.GetUUID())
res, err := agnetHTTPClient().Do(req)
res, err := agentHTTPClient().Do(req)
if err != nil {
common.SysLog("markBillingProviderNewapi: send failed: " + err.Error())
return
@@ -228,10 +228,10 @@ func markBillingProviderNewapi(email string) {
}(email, tok)
}
func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
func syncLocalUserFromAgent(me agentMeEnvelope) (*model.User, error) {
email := strings.TrimSpace(me.Data.Email)
if email == "" {
return nil, errors.New("Agnet account has no email")
return nil, errors.New("Agent account has no email")
}
var user model.User
@@ -241,7 +241,7 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
if display == "" {
display = strings.Split(email, "@")[0]
}
// Local model-access bucket — never seed with the Agnet channelId
// Local model-access bucket — never seed with the Agent channelId
// (no abilities row matches a random UUID, so the new user would land
// with zero models on first /v1/models call). Admins control group
// from the NewAPI dashboard after JIT-create. Fix companion to
@@ -253,12 +253,12 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
Password: common.GetRandomString(32),
DisplayName: display,
Email: email,
Role: roleFromAgnetWithEmail(me.Data.Role, email),
Status: statusFromAgnet(me.Data.Status),
Role: roleFromAgentWithEmail(me.Data.Role, email),
Status: statusFromAgent(me.Data.Status),
Group: group,
}
if nu.Status != common.UserStatusEnabled {
return nil, errors.New("Agnet account is not active")
return nil, errors.New("Agent account is not active")
}
if err := nu.Insert(0); err != nil {
// Possible race: duplicate email/username — reload.
@@ -283,10 +283,10 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
user.DisplayName = name
changed = true
}
// Don't overwrite the existing user's group with the Agnet channelId on
// Don't overwrite the existing user's group with the Agent channelId on
// every login: NewAPI's `users.group` is the **local model-access bucket**
// (must match a row in the `abilities` / `channels` group column to expose
// any models). The Agnet channelId is a cross-platform identity that
// any models). The Agent channelId is a cross-platform identity that
// rarely matches a NewAPI-side group, so overwriting strands the user
// with zero models. mcp-server side already tracks channelId separately
// (see markBillingProviderNewapi), so we don't need it duplicated here.
@@ -295,8 +295,8 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
// is new and has no admin-set group yet). After that, NewAPI admins own
// the group via the dashboard.
_ = me.Data.ChannelID
// Promote role from Agnet / email whitelist on every login (never demote).
desiredRole := roleFromAgnetWithEmail(me.Data.Role, email)
// Promote role from Agent / email whitelist on every login (never demote).
desiredRole := roleFromAgentWithEmail(me.Data.Role, email)
if desiredRole > user.Role {
user.Role = desiredRole
changed = true
@@ -313,9 +313,9 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
return &user, nil
}
// HeicodeAgnetSessionLogin establishes Manager session after Agnet identity verified via token(s).
func HeicodeAgnetSessionLogin(c *gin.Context) {
var req HeicodeAgnetSessionRequest
// HeicodeAgentSessionLogin establishes Manager session after Agent identity verified via token(s).
func HeicodeAgentSessionLogin(c *gin.Context) {
var req HeicodeAgentSessionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid params"})
return
@@ -328,19 +328,19 @@ func HeicodeAgnetSessionLogin(c *gin.Context) {
}
baseURL := defaultHeicodeAuthBaseURL()
me, status, err := fetchAgnetMe(baseURL, access)
me, status, err := fetchAgentMe(baseURL, access)
newAccess := ""
newRefresh := ""
if (err != nil || status == http.StatusUnauthorized || !me.Success) && refresh != "" {
na, nr, refErr := fetchAgnetRefresh(baseURL, refresh)
na, nr, refErr := fetchAgentRefresh(baseURL, refresh)
if refErr != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": refErr.Error()})
return
}
newAccess = na
newRefresh = nr
me, _, err = fetchAgnetMe(baseURL, newAccess)
me, _, err = fetchAgentMe(baseURL, newAccess)
}
if err != nil {
@@ -353,13 +353,13 @@ func HeicodeAgnetSessionLogin(c *gin.Context) {
msg = me.Detail
}
if msg == "" {
msg = "unable to verify identity with Agnet"
msg = "unable to verify identity with Agent"
}
c.JSON(http.StatusOK, gin.H{"success": false, "message": msg})
return
}
user, err := syncLocalUserFromAgnet(me)
user, err := syncLocalUserFromAgent(me)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
return
@@ -0,0 +1,50 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
)
// HeicodeCapabilities exposes the client-facing mode + model catalog so the
// desktop client can discover which modes (Sub Agile / Swarm) are available and
// how each selects models (unified spec §6: GET /api/heicode/capabilities).
//
// It is intentionally unauthenticated catalog data: it returns no user-specific
// information, only the static mode contract plus whether each runtime is
// currently wired, and the production-aligned default model.
func HeicodeCapabilities(c *gin.Context) {
subCfg := agentRuntimeClientConfigForMode(agentRuntimeModeAgent)
swarmCfg := agentRuntimeClientConfigForMode(agentRuntimeModeSwarm)
defaultModel := defaultAgentModelID()
common.ApiSuccess(c, gin.H{
"modes": []gin.H{
{
"id": "sub_agile",
"name": "Sub Agile",
"runtime_kind": "agent_management",
"model_selection": "per_role",
"supports_roles": true,
"supports_task_graph": false,
"supports_artifacts": true,
"supports_continue_chat": true,
"enabled": subCfg.Enabled,
},
{
"id": "swarm",
"name": "Swarm",
"runtime_kind": "heicode_swarm",
"model_selection": "primary",
"supports_roles": false,
"supports_task_graph": true,
"supports_artifacts": true,
"supports_continue_chat": true,
"enabled": swarmCfg.Enabled,
},
},
"models": []gin.H{
{"id": defaultModel, "name": defaultModel, "available": true},
},
})
}
@@ -0,0 +1,59 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// HeicodeTaskWorkflow projects a deployment into the client-facing "workflow"
// shape consumed by the desktop right-hand task panel (unified spec §7.9 /
// §10.2). task_id is the deployment_id. Status is the Manager-judged
// display_status, so the client never has to interpret raw runtime state.
func HeicodeTaskWorkflow(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
record = withDisplayStatus(record)
artifacts, _ := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
artItems := make([]gin.H, 0, len(artifacts))
for _, a := range artifacts {
artItems = append(artItems, gin.H{
"artifact_id": a.ArtifactID,
"title": a.Title,
"artifact_type": a.ArtifactType,
"summary": a.Summary,
})
}
agents := make([]gin.H, 0, len(record.AgentInstances))
for _, inst := range record.AgentInstances {
agents = append(agents, gin.H{
"agent_id": inst.InstanceID,
"name": inst.Role,
"role": inst.Role,
"status": firstNonEmpty(inst.RuntimeState, inst.Phase),
})
}
common.ApiSuccess(c, gin.H{
"workflow_id": record.DeploymentID,
"task_id": record.DeploymentID,
"deployment_id": record.DeploymentID,
"mode": firstNonEmpty(record.SubMode, "agile"),
"title": firstNonEmpty(record.Plan.Objective, record.DeploymentID),
"summary": record.Plan.Objective,
"status": record.DisplayStatus,
"display_status": record.DisplayStatus,
"phase": record.Phase,
"agent_count": len(record.AgentInstances),
"agents": agents,
"artifacts": artItems,
})
}
+12 -12
View File
@@ -94,7 +94,7 @@ type resourceGrantPayload struct {
BindingScope string `json:"binding_scope"`
ResourceId int `json:"resource_id"`
Role string `json:"role"`
AgnetId string `json:"agnet_id"`
AgentId string `json:"agent_id"`
PermissionScope map[string]any `json:"permission_scope"`
Constraints map[string]any `json:"constraints"`
Status string `json:"status"`
@@ -108,7 +108,7 @@ type resourceGrantResponse struct {
BindingScope string `json:"binding_scope"`
ResourceId int `json:"resource_id"`
Role string `json:"role"`
AgnetId string `json:"agnet_id"`
AgentId string `json:"agent_id"`
PermissionScope map[string]any `json:"permission_scope"`
Constraints map[string]any `json:"constraints"`
Status string `json:"status"`
@@ -190,7 +190,7 @@ func normalizeResourceGrantPayload(p resourceGrantPayload) (resourceGrantPayload
p.ProjectId = strings.TrimSpace(p.ProjectId)
p.BindingScope = strings.TrimSpace(p.BindingScope)
p.Role = strings.TrimSpace(p.Role)
p.AgnetId = strings.TrimSpace(p.AgnetId)
p.AgentId = strings.TrimSpace(p.AgentId)
p.Status = strings.ToLower(strings.TrimSpace(p.Status))
if p.ResourceId <= 0 {
@@ -199,8 +199,8 @@ func normalizeResourceGrantPayload(p resourceGrantPayload) (resourceGrantPayload
if p.Role == "" {
return p, errors.New("role required")
}
if p.AgnetId == "" {
return p, errors.New("agnet_id required")
if p.AgentId == "" {
return p, errors.New("agent_id required")
}
if p.Status == "" {
p.Status = "active"
@@ -375,7 +375,7 @@ func resourceGrantToResponse(grant model.ResourceGrant, resource *model.Resource
BindingScope: grant.BindingScope,
ResourceId: grant.ResourceId,
Role: grant.Role,
AgnetId: grant.AgnetId,
AgentId: grant.AgentId,
PermissionScope: unmarshalResourceJSON(grant.PermissionScope),
Constraints: unmarshalResourceJSON(grant.Constraints),
Status: grant.Status,
@@ -719,8 +719,8 @@ func ListResourceGrants(c *gin.Context) {
if bindingScope := strings.TrimSpace(c.Query("binding_scope")); bindingScope != "" {
query = query.Where("binding_scope = ?", bindingScope)
}
if agnetId := strings.TrimSpace(c.Query("agnet_id")); agnetId != "" {
query = query.Where("agnet_id = ?", agnetId)
if agentId := strings.TrimSpace(c.Query("agent_id")); agentId != "" {
query = query.Where("agent_id = ?", agentId)
}
if resourceId := strings.TrimSpace(c.Query("resource_id")); resourceId != "" {
query = query.Where("resource_id = ?", resourceId)
@@ -743,7 +743,7 @@ func ListResourceGrants(c *gin.Context) {
func GenerateResourceGrantManifest(c *gin.Context) {
userId := c.GetInt("id")
bindingScope := strings.TrimSpace(c.Query("binding_scope"))
agentId := strings.TrimSpace(c.Query("agnet_id"))
agentId := strings.TrimSpace(c.Query("agent_id"))
role := strings.TrimSpace(c.Query("role"))
query := model.DB.Where("user_id = ? AND status = ?", userId, "active")
@@ -751,7 +751,7 @@ func GenerateResourceGrantManifest(c *gin.Context) {
query = query.Where("binding_scope = ?", bindingScope)
}
if agentId != "" {
query = query.Where("agnet_id = ?", agentId)
query = query.Where("agent_id = ?", agentId)
}
if role != "" {
query = query.Where("role = ?", role)
@@ -815,7 +815,7 @@ func CreateResourceGrant(c *gin.Context) {
BindingScope: payload.BindingScope,
ResourceId: payload.ResourceId,
Role: payload.Role,
AgnetId: payload.AgnetId,
AgentId: payload.AgentId,
PermissionScope: permissionScope,
Constraints: constraints,
Status: payload.Status,
@@ -864,7 +864,7 @@ func UpdateResourceGrant(c *gin.Context) {
grant.BindingScope = payload.BindingScope
grant.ResourceId = payload.ResourceId
grant.Role = payload.Role
grant.AgnetId = payload.AgnetId
grant.AgentId = payload.AgentId
grant.PermissionScope = permissionScope
grant.Constraints = constraints
grant.Status = payload.Status
+11 -11
View File
@@ -120,7 +120,7 @@ func TestDeleteResourceRevokesBindingAndActiveGrants(t *testing.T) {
BindingScope: "project-alpha",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agent-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["repo:read"]}`,
Status: "active",
}
@@ -159,7 +159,7 @@ func TestDeleteResourceRevokesBindingAndActiveGrants(t *testing.T) {
require.Contains(t, listActive.Body.String(), `"items":[]`)
}
func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
func TestCreateResourceGrantAssignsBoundResourceToRoleAgent(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
@@ -177,7 +177,7 @@ func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
"binding_scope":"https://example.com/sk.git#main",
"resource_id":%d,
"role":"developer",
"agnet_id":"agnet-dev-1",
"agent_id":"agent-dev-1",
"permission_scope":{"actions":["read"]},
"constraints":{"paths":["skills/**"]}
}`, resource.Id)
@@ -187,14 +187,14 @@ func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"https://example.com/sk.git#main"`)
require.Contains(t, w.Body.String(), `"role":"developer"`)
require.Contains(t, w.Body.String(), `"agnet_id":"agnet-dev-1"`)
require.Contains(t, w.Body.String(), `"agent_id":"agent-dev-1"`)
var grant model.ResourceGrant
require.NoError(t, db.First(&grant).Error)
require.Equal(t, resource.Id, grant.ResourceId)
require.Equal(t, "https://example.com/sk.git#main", grant.BindingScope)
require.Equal(t, "developer", grant.Role)
require.Equal(t, "agnet-dev-1", grant.AgnetId)
require.Equal(t, "agent-dev-1", grant.AgentId)
}
func TestCreateResourceGrantRejectsMismatchedBindingScope(t *testing.T) {
@@ -212,7 +212,7 @@ func TestCreateResourceGrantRejectsMismatchedBindingScope(t *testing.T) {
"binding_scope":"azure-vm-dev",
"resource_id":%d,
"role":"operator",
"agnet_id":"agnet-ops-1"
"agent_id":"agent-ops-1"
}`, resource.Id)
w := performResourceRequest(CreateResourceGrant, 7, http.MethodPost, "/", body)
@@ -241,7 +241,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
BindingScope: "repo-main",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agnet-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["read"]}`,
Constraints: `{"paths":["heicode/controller/**"]}`,
Status: "active",
@@ -251,7 +251,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
BindingScope: "repo-main",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agnet-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["write"]}`,
Status: "revoked",
}).Error)
@@ -261,7 +261,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
7,
http.MethodGet,
"/manifest",
"/manifest?binding_scope=repo-main&role=backend&agnet_id=agnet-backend-1",
"/manifest?binding_scope=repo-main&role=backend&agent_id=agent-backend-1",
"",
)
@@ -269,7 +269,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"repo-main"`)
require.Contains(t, w.Body.String(), `"agent_role":"backend"`)
require.Contains(t, w.Body.String(), `"target_agent_ref":"agnet-backend-1"`)
require.Contains(t, w.Body.String(), `"target_agent_ref":"agent-backend-1"`)
require.Contains(t, w.Body.String(), `"resource_type":"git"`)
require.Contains(t, w.Body.String(), `"allowed_actions":["read"]`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main"`)
@@ -293,7 +293,7 @@ func TestDeleteResourceGrantRevokesInsteadOfDeleting(t *testing.T) {
BindingScope: "project-alpha",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agent-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["repo:read","repo:write"]}`,
Constraints: `{"paths":["src/**"]}`,
Status: "active",
+10 -10
View File
@@ -45,7 +45,7 @@ services:
- NODE_NAME=heicode-node-1
# 默认与 docs/integration/Heicode-登录接口对接文档.md §2.1 一致;覆盖仅用于非标准网关。
- HEICODE_AUTH_BASE_URL=${HEICODE_AUTH_BASE_URL:-https://apimtaiji.azure-api.net/api/mcp}
# Agnet 登录后 JIT 同步:邮箱命中以下白名单则自动提权
# Agent 登录后 JIT 同步:邮箱命中以下白名单则自动提权
- HEICODE_ROOT_EMAILS=${HEICODE_ROOT_EMAILS:-}
- HEICODE_ADMIN_EMAILS=${HEICODE_ADMIN_EMAILS:-}
# Long-lived resource credentials are written to Azure Key Vault via
@@ -54,18 +54,18 @@ services:
- AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}
# Agent Manager Runtime: use the current production IP directly.
# The public domain is not used until DNS/HTTPS is fixed.
- AGNET_RUNTIME_ENABLED=${AGNET_RUNTIME_ENABLED:-false}
- AGNET_RUNTIME_BASE_URL=${AGNET_RUNTIME_BASE_URL:-http://20.212.121.126}
- AGNET_RUNTIME_CREATE_PATH=${AGNET_RUNTIME_CREATE_PATH:-/api/agnet/deployments}
- AGNET_RUNTIME_HEALTH_PATH=${AGNET_RUNTIME_HEALTH_PATH:-/api/agnet/health}
- AGNET_RUNTIME_STOP_PATH=${AGNET_RUNTIME_STOP_PATH:-/api/agnet/deployments/{deployment_id}/stop}
- AGNET_RUNTIME_SERVICE_TOKEN=${AGNET_RUNTIME_SERVICE_TOKEN:-}
- AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF=${AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF:-}
# HeiCode-Swarm Runtime is separate from ordinary sub Agnet Runtime.
- AGENT_RUNTIME_ENABLED=${AGENT_RUNTIME_ENABLED:-false}
- AGENT_RUNTIME_BASE_URL=${AGENT_RUNTIME_BASE_URL:-http://20.212.121.126}
- AGENT_RUNTIME_CREATE_PATH=${AGENT_RUNTIME_CREATE_PATH:-/api/agent/deployments}
- AGENT_RUNTIME_HEALTH_PATH=${AGENT_RUNTIME_HEALTH_PATH:-/api/agent/health}
- AGENT_RUNTIME_STOP_PATH=${AGENT_RUNTIME_STOP_PATH:-/api/agent/deployments/{deployment_id}/stop}
- AGENT_RUNTIME_SERVICE_TOKEN=${AGENT_RUNTIME_SERVICE_TOKEN:-}
- AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF=${AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF:-}
# HeiCode-Swarm Runtime is separate from ordinary sub Agent Runtime.
- SWARM_RUNTIME_ENABLED=${SWARM_RUNTIME_ENABLED:-false}
- SWARM_RUNTIME_BASE_URL=${SWARM_RUNTIME_BASE_URL:-}
- SWARM_RUNTIME_CREATE_PATH=${SWARM_RUNTIME_CREATE_PATH:-/api/swarms}
- SWARM_RUNTIME_HEALTH_PATH=${SWARM_RUNTIME_HEALTH_PATH:-/api/agnet/health}
- SWARM_RUNTIME_HEALTH_PATH=${SWARM_RUNTIME_HEALTH_PATH:-/api/agent/health}
- SWARM_RUNTIME_STOP_PATH=${SWARM_RUNTIME_STOP_PATH:-/api/swarms/{swarm_id}/stop}
- SWARM_RUNTIME_APPROVAL_DECISION_PATH=${SWARM_RUNTIME_APPROVAL_DECISION_PATH:-/api/swarms/{swarm_id}/approvals/{approval_id}}
- SWARM_RUNTIME_SERVICE_TOKEN=${SWARM_RUNTIME_SERVICE_TOKEN:-}
@@ -12,7 +12,7 @@ Security rule: never put real passwords, tokens, SSH keys, Redis keys, PostgreSQ
| `heicode/docker-compose.azure-vm.yml` | Azure VM Manager service | Runs only `heicode`; PostgreSQL and Redis are expected to be managed Azure services. |
| `heicode/docker-compose.override.yml` | Local-source image override | Builds `heicode-manager:local` from the checked-out repo. Keep it in the compose file list when deploying this repo state. |
| `heicode/bin/azure_vm_deploy.sh` | SSH deployment helper | Uses env vars only; can fast-forward a remote branch, performs remote compose up, health gate, and rollback pointer capture. |
| `heicode/bin/acceptance_agnet_local.sh` | Local Agnet control-plane smoke/acceptance probe | Requires an admin session cookie supplied via env; does not store credentials. |
| `heicode/bin/acceptance_agent_local.sh` | Local Agent control-plane smoke/acceptance probe | Requires an admin session cookie supplied via env; does not store credentials. |
| `heicode/.env.example` | Env-var reference | Placeholder-only reference; production `.env` must stay on the VM and out of Git. |
## 2. Required VM inputs
@@ -114,9 +114,9 @@ The script will:
6. Poll `/api/status` through the VM-local health URL.
7. Update `.last_success_image` only after the health gate passes.
## 5.1 Agnet operator handoff
## 5.1 Agent operator handoff
When Agnet is the executor, Manager should create a high-risk `newapi-rebuild-deploy` deployment using `docs/integration/agnet-platform-request-contract.md` and pass only references:
When Agent is the executor, Manager should create a high-risk `newapi-rebuild-deploy` deployment using `docs/integration/agent-platform-request-contract.md` and pass only references:
| Field | Required reference |
|---|---|
@@ -126,7 +126,7 @@ When Agnet is the executor, Manager should create a high-risk `newapi-rebuild-de
| `orchestration_plan.constraints.healthcheck_url_ref` | `env://NEWAPI_HEALTHCHECK_URL`, expected to resolve to the VM-local `/api/status` probe. |
| `orchestration_plan.metadata.commit` | Intended Git commit or branch to deploy, such as `origin/main` after push. |
Minimum evidence Agnet must return before Manager marks the operation deployed:
Minimum evidence Agent must return before Manager marks the operation deployed:
1. Remote commit after fetch/pull.
2. `docker compose ... ps` status for `heicode`.
@@ -149,14 +149,14 @@ curl -fsS http://127.0.0.1:3000/api/status
Expected health response includes `"success":true`.
Optional Manager / Agnet smoke probe after obtaining a safe admin session cookie without logging it:
Optional Manager / Agent smoke probe after obtaining a safe admin session cookie without logging it:
```bash
AUTH_COOKIE='REDACTED_SESSION_COOKIE' \
BASE_URL='http://127.0.0.1:3000' \
TENANT_ID='tenant_smoke' \
PROJECT_ID='project_smoke' \
./bin/acceptance_agnet_local.sh
./bin/acceptance_agent_local.sh
```
Do not commit or report the real cookie.
+4 -4
View File
@@ -29,13 +29,13 @@ import (
func TestUserOrV2DeviceAuthRejectsMalformedEncryptedRequest(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/api/agnet/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
router.POST("/api/agent/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
})
req := httptest.NewRequest(
http.MethodPost,
"/api/agnet/user/tasks/task-v2/deployment-draft",
"/api/agent/user/tasks/task-v2/deployment-draft",
nil,
)
req.Header.Set("Content-Encoding", V2ContentEncoding)
@@ -100,7 +100,7 @@ func TestUserOrV2DeviceAuthDecryptsValidEncryptedRequest(t *testing.T) {
}
router := gin.New()
router.POST("/api/agnet/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
router.POST("/api/agent/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
if got := c.GetInt("id"); got != 42 {
t.Fatalf("id context = %d, want 42", got)
}
@@ -116,7 +116,7 @@ func TestUserOrV2DeviceAuthDecryptsValidEncryptedRequest(t *testing.T) {
c.JSON(http.StatusOK, gin.H{"success": true})
})
path := "/api/agnet/user/tasks/task-v2/deployment-draft"
path := "/api/agent/user/tasks/task-v2/deployment-draft"
body := []byte(`{"objective":"encrypted sub task"}`)
req := newEncryptedV2Request(t, http.MethodPost, path, body, deviceID, fingerprint, privateKey)
rec := httptest.NewRecorder()
@@ -1,10 +1,10 @@
package model
// AgnetApprovalRequest records a user-visible approval gate for a
// high-risk Agnet operation. It intentionally stores only a Secret
// AgentApprovalRequest records a user-visible approval gate for a
// high-risk Agent operation. It intentionally stores only a Secret
// Store reference for credential-backed operations; plaintext secrets
// must never be written to this table.
type AgnetApprovalRequest struct {
type AgentApprovalRequest struct {
Id int `json:"id" gorm:"primaryKey"`
ApprovalID string `json:"approval_id" gorm:"type:varchar(64);uniqueIndex;not null"`
UserId int `json:"user_id" gorm:"index;not null"`
@@ -31,14 +31,14 @@ type AgnetApprovalRequest struct {
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
}
func (AgnetApprovalRequest) TableName() string {
return "agnet_approval_requests"
func (AgentApprovalRequest) TableName() string {
return "agent_approval_requests"
}
// AgnetCredentialLease is the Manager-side short-lived credential
// AgentCredentialLease is the Manager-side short-lived credential
// handle produced after an approval succeeds. CredentialRef is the
// external handle; SecretRef is internal and points at Azure Key Vault.
type AgnetCredentialLease struct {
type AgentCredentialLease struct {
Id int `json:"id" gorm:"primaryKey"`
LeaseID string `json:"lease_id" gorm:"type:varchar(64);uniqueIndex;not null"`
CredentialRef string `json:"credential_ref" gorm:"type:varchar(128);uniqueIndex;not null"`
@@ -59,6 +59,6 @@ type AgnetCredentialLease struct {
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
}
func (AgnetCredentialLease) TableName() string {
return "agnet_credential_leases"
func (AgentCredentialLease) TableName() string {
return "agent_credential_leases"
}
@@ -7,7 +7,7 @@ import (
"gorm.io/gorm"
)
type AgnetArtifact struct {
type AgentArtifact struct {
Id int `gorm:"primaryKey" json:"id"`
ArtifactID string `gorm:"type:varchar(128);uniqueIndex" json:"artifact_id"`
DeploymentID string `gorm:"type:varchar(64);index" json:"deployment_id"`
@@ -24,22 +24,22 @@ type AgnetArtifact struct {
CreatedAtMs int64 `gorm:"bigint;index" json:"created_at_ms"`
}
func (AgnetArtifact) TableName() string {
return "agnet_artifacts"
func (AgentArtifact) TableName() string {
return "agent_artifacts"
}
type ListAgnetArtifactsFilter struct {
type ListAgentArtifactsFilter struct {
DeploymentID string
TaskID string
CorrelationID string
Limit int
}
func UpsertAgnetArtifact(row *AgnetArtifact) error {
func UpsertAgentArtifact(row *AgentArtifact) error {
if DB == nil || row == nil {
return nil
}
var existing AgnetArtifact
var existing AgentArtifact
if row.ArtifactID != "" {
if err := DB.Where("artifact_id = ?", row.ArtifactID).First(&existing).Error; err == nil {
row.Id = existing.Id
@@ -49,11 +49,11 @@ func UpsertAgnetArtifact(row *AgnetArtifact) error {
return DB.Create(row).Error
}
func ListAgnetArtifacts(f ListAgnetArtifactsFilter) ([]AgnetArtifact, error) {
func ListAgentArtifacts(f ListAgentArtifactsFilter) ([]AgentArtifact, error) {
if DB == nil {
return nil, nil
}
q := DB.Model(&AgnetArtifact{})
q := DB.Model(&AgentArtifact{})
if f.DeploymentID != "" {
q = q.Where("deployment_id = ?", f.DeploymentID)
}
@@ -67,13 +67,13 @@ func ListAgnetArtifacts(f ListAgnetArtifactsFilter) ([]AgnetArtifact, error) {
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgnetArtifact
var items []AgentArtifact
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
return items, err
}
func GetAgnetArtifactByDeployment(deploymentID string, artifactID string) (AgnetArtifact, bool, error) {
var row AgnetArtifact
func GetAgentArtifactByDeployment(deploymentID string, artifactID string) (AgentArtifact, bool, error) {
var row AgentArtifact
if DB == nil {
return row, false, nil
}
@@ -6,15 +6,15 @@ import (
"github.com/heicode/manager/common"
)
// AgnetAuditEvent is the persistent audit-trail row for the Agnet
// AgentAuditEvent is the persistent audit-trail row for the Agent
// control-plane. Each row records one observable transition in the
// Agnet lifecycle — deployment accepted, instance phase changed, SK
// Agent lifecycle — deployment accepted, instance phase changed, SK
// snapshot refreshed, etc. — so admins can answer the "who / when /
// for which task / against which resource / with what result" set of
// questions even after the Manager container restarts.
//
// Before this table existed the control-plane stashed events in an
// in-process `map[string][]agnetEvent` (controller/agnet_control_plane
// in-process `map[string][]agentEvent` (controller/agent_control_plane
// .go:219). Every restart wiped audit history — unacceptable for a
// product where the audit page is part of the security story.
//
@@ -32,7 +32,7 @@ import (
// Cross-DB compatibility (CLAUDE.md Rule 2): GORM AutoMigrate maps the
// tags to the correct types on SQLite / MySQL / PostgreSQL. No raw
// SQL. No DB-specific column types.
type AgnetAuditEvent struct {
type AgentAuditEvent struct {
Id int `gorm:"primaryKey" json:"id"`
EventID string `gorm:"type:varchar(64);uniqueIndex" json:"event_id"`
Event string `gorm:"type:varchar(64);index" json:"event"`
@@ -52,17 +52,17 @@ type AgnetAuditEvent struct {
// TableName pins the migration target so we can rename the Go struct
// later without breaking the deployed schema.
func (AgnetAuditEvent) TableName() string {
return "agnet_audit_events"
func (AgentAuditEvent) TableName() string {
return "agent_audit_events"
}
// InsertAgnetAuditEvent best-effort persists one audit row. Callers
// InsertAgentAuditEvent best-effort persists one audit row. Callers
// invoke this in a hot path (right after mutating a deployment), so:
// - errors are logged but never returned — the audit write must NOT
// fail the user-facing API
// - DB is nil-guarded so unit tests / partial-init binaries don't
// panic on a missing connection
func InsertAgnetAuditEvent(evt *AgnetAuditEvent) {
func InsertAgentAuditEvent(evt *AgentAuditEvent) {
if DB == nil || evt == nil {
return
}
@@ -76,14 +76,14 @@ func InsertAgnetAuditEvent(evt *AgnetAuditEvent) {
evt.Result = "ok"
}
if err := DB.Create(evt).Error; err != nil {
common.SysLog("InsertAgnetAuditEvent: " + err.Error())
common.SysLog("InsertAgentAuditEvent: " + err.Error())
}
}
// ListAgnetAuditEventsFilter narrows the audit query to a slice of
// ListAgentAuditEventsFilter narrows the audit query to a slice of
// dashboard relevant rows. Zero-value fields are ignored — callers
// pass only the filters they care about.
type ListAgnetAuditEventsFilter struct {
type ListAgentAuditEventsFilter struct {
UserID string
BindingScope string
DeploymentID string
@@ -94,14 +94,14 @@ type ListAgnetAuditEventsFilter struct {
Offset int
}
// ListAgnetAuditEvents pages over audit rows ordered newest-first.
// Used by the /api/agnet/audit-logs endpoint and the future task-
// ListAgentAuditEvents pages over audit rows ordered newest-first.
// Used by the /api/agent/audit-logs endpoint and the future task-
// scoped audit drawer.
func ListAgnetAuditEvents(f ListAgnetAuditEventsFilter) ([]AgnetAuditEvent, int64, error) {
func ListAgentAuditEvents(f ListAgentAuditEventsFilter) ([]AgentAuditEvent, int64, error) {
if DB == nil {
return nil, 0, nil
}
q := DB.Model(&AgnetAuditEvent{})
q := DB.Model(&AgentAuditEvent{})
if f.UserID != "" {
q = q.Where("user_id = ?", f.UserID)
}
@@ -128,19 +128,19 @@ func ListAgnetAuditEvents(f ListAgnetAuditEventsFilter) ([]AgnetAuditEvent, int6
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgnetAuditEvent
var items []AgentAuditEvent
err := q.Order("occurred_at desc, id desc").Limit(limit).Offset(f.Offset).Find(&items).Error
return items, total, err
}
// ListAgnetAuditEventsByDeployment is the hot path for the deployment
// ListAgentAuditEventsByDeployment is the hot path for the deployment
// detail drawer — returns all events for one deployment in chronological
// order so the timeline reads top-to-bottom.
func ListAgnetAuditEventsByDeployment(deploymentID string) ([]AgnetAuditEvent, error) {
func ListAgentAuditEventsByDeployment(deploymentID string) ([]AgentAuditEvent, error) {
if DB == nil || deploymentID == "" {
return nil, nil
}
var items []AgnetAuditEvent
var items []AgentAuditEvent
err := DB.Where("deployment_id = ?", deploymentID).
Order("occurred_at asc, id asc").
Find(&items).Error
@@ -15,24 +15,24 @@ import (
func setupAuditTest(t *testing.T) {
t.Helper()
if err := DB.AutoMigrate(&AgnetAuditEvent{}); err != nil {
if err := DB.AutoMigrate(&AgentAuditEvent{}); err != nil {
t.Fatalf("migrate: %v", err)
}
if err := DB.Exec("DELETE FROM agnet_audit_events").Error; err != nil {
if err := DB.Exec("DELETE FROM agent_audit_events").Error; err != nil {
t.Fatalf("truncate: %v", err)
}
}
func TestInsertAgnetAuditEvent_PersistsRow(t *testing.T) {
func TestInsertAgentAuditEvent_PersistsRow(t *testing.T) {
// Regression for H1 — the previous implementation appended to an
// in-process map that was wiped on every container restart. This
// test pins the new behaviour: rows survive in DB.
setupAuditTest(t)
evt := &AgnetAuditEvent{
evt := &AgentAuditEvent{
EventID: "evt_test_001",
Event: "deployment.accepted",
Actor: "agnet_control_plane",
Actor: "agent_control_plane",
Resource: "dep_abc",
UserID: "user-42",
ChannelID: "channel-1",
@@ -41,9 +41,9 @@ func TestInsertAgnetAuditEvent_PersistsRow(t *testing.T) {
CorrelationID: "corr-xyz",
RequestID: "req-001",
}
InsertAgnetAuditEvent(evt)
InsertAgentAuditEvent(evt)
var got AgnetAuditEvent
var got AgentAuditEvent
if err := DB.Where("event_id = ?", "evt_test_001").First(&got).Error; err != nil {
t.Fatalf("not persisted: %v", err)
}
@@ -58,24 +58,24 @@ func TestInsertAgnetAuditEvent_PersistsRow(t *testing.T) {
}
}
func TestInsertAgnetAuditEvent_NilGuards(t *testing.T) {
func TestInsertAgentAuditEvent_NilGuards(t *testing.T) {
// Production safety: audit writes run inside hot paths (right
// after a deployment mutation). A nil DB or nil event MUST NOT
// panic — better to drop the audit row than to fail the user
// API call.
defer func() {
if r := recover(); r != nil {
t.Fatalf("InsertAgnetAuditEvent panicked: %v", r)
t.Fatalf("InsertAgentAuditEvent panicked: %v", r)
}
}()
InsertAgnetAuditEvent(nil) // nil evt
InsertAgentAuditEvent(nil) // nil evt
prev := DB
DB = nil
InsertAgnetAuditEvent(&AgnetAuditEvent{EventID: "x"}) // nil DB
InsertAgentAuditEvent(&AgentAuditEvent{EventID: "x"}) // nil DB
DB = prev
}
func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
func TestListAgentAuditEvents_FilterAndPaginate(t *testing.T) {
setupAuditTest(t)
now := time.Now().UnixMilli()
@@ -84,7 +84,7 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
if i%2 == 0 {
uid = "user-B"
}
InsertAgnetAuditEvent(&AgnetAuditEvent{
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: fmt.Sprintf("evt_%d", i),
Event: "deployment.accepted",
UserID: uid,
@@ -94,7 +94,7 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
}
// All rows visible without filter.
rows, total, err := ListAgnetAuditEvents(ListAgnetAuditEventsFilter{})
rows, total, err := ListAgentAuditEvents(ListAgentAuditEventsFilter{})
if err != nil {
t.Fatalf("list: %v", err)
}
@@ -103,7 +103,7 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
}
// user_id filter narrows to 3 (i=0,2,4 → user-B).
_, total, err = ListAgnetAuditEvents(ListAgnetAuditEventsFilter{UserID: "user-B"})
_, total, err = ListAgentAuditEvents(ListAgentAuditEventsFilter{UserID: "user-B"})
if err != nil {
t.Fatalf("list with userid: %v", err)
}
@@ -112,28 +112,28 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
}
// Newest-first ordering. evt_4 inserted last → top.
rows, _, _ = ListAgnetAuditEvents(ListAgnetAuditEventsFilter{})
rows, _, _ = ListAgentAuditEvents(ListAgentAuditEventsFilter{})
if rows[0].EventID != "evt_4" {
t.Errorf("expected newest-first, got %q on top", rows[0].EventID)
}
}
func TestListAgnetAuditEventsByDeployment_Chronological(t *testing.T) {
func TestListAgentAuditEventsByDeployment_Chronological(t *testing.T) {
// Detail-drawer reads need oldest-first for a top-to-bottom
// timeline. Confirms ascending order independent of insertion
// order.
setupAuditTest(t)
InsertAgnetAuditEvent(&AgnetAuditEvent{
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: "evt_late", Event: "x", DeploymentID: "dep_T",
OccurredAt: 9000,
})
InsertAgnetAuditEvent(&AgnetAuditEvent{
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: "evt_early", Event: "x", DeploymentID: "dep_T",
OccurredAt: 1000,
})
rows, err := ListAgnetAuditEventsByDeployment("dep_T")
rows, err := ListAgentAuditEventsByDeployment("dep_T")
if err != nil {
t.Fatalf("query: %v", err)
}
@@ -2,7 +2,7 @@ package model
import "errors"
type AgnetCallbackEvent struct {
type AgentCallbackEvent struct {
Id int `gorm:"primaryKey" json:"id"`
EventID string `gorm:"type:varchar(128);uniqueIndex" json:"event_id"`
IdempotencyKey string `gorm:"type:varchar(128);index" json:"idempotency_key"`
@@ -22,25 +22,25 @@ type AgnetCallbackEvent struct {
CreatedAtMs int64 `gorm:"bigint;index" json:"created_at_ms"`
}
func (AgnetCallbackEvent) TableName() string {
return "agnet_callback_events"
func (AgentCallbackEvent) TableName() string {
return "agent_callback_events"
}
type ListAgnetCallbackEventsFilter struct {
type ListAgentCallbackEventsFilter struct {
DeploymentID string
TaskID string
CorrelationID string
Limit int
}
func InsertAgnetCallbackEvent(row *AgnetCallbackEvent) (bool, error) {
func InsertAgentCallbackEvent(row *AgentCallbackEvent) (bool, error) {
if DB == nil || row == nil {
return false, nil
}
if row.EventID == "" {
return false, errors.New("event_id is required")
}
var existing AgnetCallbackEvent
var existing AgentCallbackEvent
if err := DB.Where("event_id = ?", row.EventID).First(&existing).Error; err == nil {
return false, nil
}
@@ -55,11 +55,11 @@ func InsertAgnetCallbackEvent(row *AgnetCallbackEvent) (bool, error) {
return true, nil
}
func ListAgnetCallbackEvents(f ListAgnetCallbackEventsFilter) ([]AgnetCallbackEvent, error) {
func ListAgentCallbackEvents(f ListAgentCallbackEventsFilter) ([]AgentCallbackEvent, error) {
if DB == nil {
return nil, nil
}
q := DB.Model(&AgnetCallbackEvent{})
q := DB.Model(&AgentCallbackEvent{})
if f.DeploymentID != "" {
q = q.Where("deployment_id = ?", f.DeploymentID)
}
@@ -73,7 +73,7 @@ func ListAgnetCallbackEvents(f ListAgnetCallbackEventsFilter) ([]AgnetCallbackEv
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgnetCallbackEvent
var items []AgentCallbackEvent
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
return items, err
}
@@ -1,10 +1,10 @@
package model
// AgnetDeployment stores the Manager-side deployment placeholder.
// It is intentionally a control-plane snapshot: the real Agnet platform
// AgentDeployment stores the Manager-side deployment placeholder.
// It is intentionally a control-plane snapshot: the real Agent platform
// execution state can attach later, but Manager must not lose the accepted
// plan, manifest, status, or audit context across container restarts.
type AgnetDeployment struct {
type AgentDeployment struct {
Id int `gorm:"primaryKey" json:"id"`
DeploymentID string `gorm:"type:varchar(64);uniqueIndex" json:"deployment_id"`
UserID string `gorm:"type:varchar(64);index" json:"user_id"`
@@ -29,6 +29,6 @@ type AgnetDeployment struct {
PayloadJSON string `gorm:"type:text" json:"payload_json"`
}
func (AgnetDeployment) TableName() string {
return "agnet_deployments"
func (AgentDeployment) TableName() string {
return "agent_deployments"
}
@@ -1,6 +1,6 @@
package model
type AgnetSKSnapshot struct {
type AgentSKSnapshot struct {
Id int `gorm:"primaryKey" json:"id"`
SnapshotID string `gorm:"type:varchar(128);uniqueIndex" json:"snapshot_id"`
DeploymentID string `gorm:"type:varchar(64);index" json:"deployment_id"`
@@ -12,22 +12,22 @@ type AgnetSKSnapshot struct {
ResolvedAtMs int64 `gorm:"bigint;index" json:"resolved_at_ms"`
}
func (AgnetSKSnapshot) TableName() string {
return "agnet_sk_snapshots"
func (AgentSKSnapshot) TableName() string {
return "agent_sk_snapshots"
}
func InsertAgnetSKSnapshots(items []AgnetSKSnapshot) error {
func InsertAgentSKSnapshots(items []AgentSKSnapshot) error {
if DB == nil || len(items) == 0 {
return nil
}
return DB.Create(&items).Error
}
func ListAgnetSKSnapshots(deploymentID string) ([]AgnetSKSnapshot, error) {
func ListAgentSKSnapshots(deploymentID string) ([]AgentSKSnapshot, error) {
if DB == nil || deploymentID == "" {
return nil, nil
}
var items []AgnetSKSnapshot
var items []AgentSKSnapshot
err := DB.Where("deployment_id = ?", deploymentID).
Order("resolved_at_ms asc, id asc").
Find(&items).Error
+53 -17
View File
@@ -247,6 +247,40 @@ func InitLogDB() (err error) {
return err
}
// renameAgnetTablesToAgent renames legacy agnet_* tables (and the
// resource_grants.agnet_id column) to their agent_* equivalents, preserving
// existing production data after the agnet->agent terminology unification.
// Idempotent and cross-DB (uses GORM Migrator). Runs before AutoMigrate so the
// renamed tables are reused instead of being recreated empty.
func renameAgnetTablesToAgent() {
m := DB.Migrator()
pairs := [][2]string{
{"agnet_approval_requests", "agent_approval_requests"},
{"agnet_credential_leases", "agent_credential_leases"},
{"agnet_deployments", "agent_deployments"},
{"agnet_callback_events", "agent_callback_events"},
{"agnet_artifacts", "agent_artifacts"},
{"agnet_sk_snapshots", "agent_sk_snapshots"},
{"agnet_audit_events", "agent_audit_events"},
}
for _, p := range pairs {
if m.HasTable(p[0]) && !m.HasTable(p[1]) {
if err := m.RenameTable(p[0], p[1]); err != nil {
common.SysLog("renameAgnetTablesToAgent: rename " + p[0] + " -> " + p[1] + ": " + err.Error())
} else {
common.SysLog("renameAgnetTablesToAgent: renamed " + p[0] + " -> " + p[1])
}
}
}
if m.HasTable("resource_grants") && m.HasColumn(&ResourceGrant{}, "agnet_id") && !m.HasColumn(&ResourceGrant{}, "agent_id") {
if err := m.RenameColumn(&ResourceGrant{}, "agnet_id", "agent_id"); err != nil {
common.SysLog("renameAgnetTablesToAgent: rename column agnet_id -> agent_id: " + err.Error())
} else {
common.SysLog("renameAgnetTablesToAgent: renamed column resource_grants.agnet_id -> agent_id")
}
}
}
func migrateDB() error {
// Migrate price_amount column from float/double to decimal for existing tables
migrateSubscriptionPlanPriceAmount()
@@ -254,6 +288,8 @@ func migrateDB() error {
if err := migrateTokenModelLimitsToText(); err != nil {
return err
}
// Preserve data across the agnet->agent rename (idempotent, runs before AutoMigrate).
renameAgnetTablesToAgent()
err := DB.AutoMigrate(
&Channel{},
@@ -283,19 +319,19 @@ func migrateDB() error {
&GitSource{},
&ResourceBinding{},
&ResourceGrant{},
&AgnetApprovalRequest{},
&AgnetCredentialLease{},
&AgnetDeployment{},
&AgnetCallbackEvent{},
&AgnetArtifact{},
&AgnetSKSnapshot{},
&AgentApprovalRequest{},
&AgentCredentialLease{},
&AgentDeployment{},
&AgentCallbackEvent{},
&AgentArtifact{},
&AgentSKSnapshot{},
// V2 device-binding: X25519 keypair the Manager uses for ECDH
// body decryption. See model/server_key.go.
&ServerKey{},
// Agnet control-plane audit trail. Replaces the previous
// in-process `agnetEvents map` that was wiped on every container
// restart. See model/agnet_audit.go for the rationale.
&AgnetAuditEvent{},
// Agent control-plane audit trail. Replaces the previous
// in-process `agentEvents map` that was wiped on every container
// restart. See model/agent_audit.go for the rationale.
&AgentAuditEvent{},
)
if err != nil {
return err
@@ -355,13 +391,13 @@ func migrateDBFast() error {
{&GitSource{}, "GitSource"},
{&ResourceBinding{}, "ResourceBinding"},
{&ResourceGrant{}, "ResourceGrant"},
{&AgnetApprovalRequest{}, "AgnetApprovalRequest"},
{&AgnetCredentialLease{}, "AgnetCredentialLease"},
{&AgnetDeployment{}, "AgnetDeployment"},
{&AgnetCallbackEvent{}, "AgnetCallbackEvent"},
{&AgnetArtifact{}, "AgnetArtifact"},
{&AgnetSKSnapshot{}, "AgnetSKSnapshot"},
{&AgnetAuditEvent{}, "AgnetAuditEvent"},
{&AgentApprovalRequest{}, "AgentApprovalRequest"},
{&AgentCredentialLease{}, "AgentCredentialLease"},
{&AgentDeployment{}, "AgentDeployment"},
{&AgentCallbackEvent{}, "AgentCallbackEvent"},
{&AgentArtifact{}, "AgentArtifact"},
{&AgentSKSnapshot{}, "AgentSKSnapshot"},
{&AgentAuditEvent{}, "AgentAuditEvent"},
}
// 动态计算migration数量,确保errChan缓冲区足够大
errChan := make(chan error, len(migrations))
+2 -2
View File
@@ -22,7 +22,7 @@ type ResourceBinding struct {
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
}
// ResourceGrant assigns a ResourceBinding to a role and child Agnet.
// ResourceGrant assigns a ResourceBinding to a role and child Agent.
// It is the auditable Manager expression of "user grants bound resource to role".
type ResourceGrant struct {
Id int `json:"id"`
@@ -32,7 +32,7 @@ type ResourceGrant struct {
BindingScope string `json:"binding_scope" gorm:"type:varchar(512);index"`
ResourceId int `json:"resource_id" gorm:"index;not null"`
Role string `json:"role" gorm:"type:varchar(128);index;not null"`
AgnetId string `json:"agnet_id" gorm:"type:varchar(128);index;not null"`
AgentId string `json:"agent_id" gorm:"type:varchar(128);index;not null"`
PermissionScope string `json:"permission_scope" gorm:"type:text"`
Constraints string `json:"constraints" gorm:"type:text"`
Status string `json:"status" gorm:"type:varchar(32);default:'active';index"`
@@ -21,7 +21,7 @@ import (
"gorm.io/gorm"
)
func setupAgnetRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
func setupAgentRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
t.Helper()
common.UsingSQLite = true
common.UsingMySQL = false
@@ -35,8 +35,8 @@ func setupAgnetRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
model.LOG_DB = db
require.NoError(t, db.AutoMigrate(
&model.User{},
&model.AgnetDeployment{},
&model.AgnetAuditEvent{},
&model.AgentDeployment{},
&model.AgentAuditEvent{},
))
adminToken := "runtime-smoke-admin-token"
require.NoError(t, db.Create(&model.User{
@@ -64,7 +64,7 @@ func setupAgnetRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
return db
}
func startAgnetRuntimeManagerSmokeServer(t *testing.T) string {
func startAgentRuntimeManagerSmokeServer(t *testing.T) string {
t.Helper()
gin.SetMode(gin.TestMode)
engine := gin.New()
@@ -82,7 +82,7 @@ func startAgnetRuntimeManagerSmokeServer(t *testing.T) string {
return "http://" + listener.Addr().String()
}
func agnetRuntimeAdminRequest(t *testing.T, method string, url string, body string) *http.Response {
func agentRuntimeAdminRequest(t *testing.T, method string, url string, body string) *http.Response {
t.Helper()
req, err := http.NewRequest(method, url, bytes.NewBufferString(body))
require.NoError(t, err)
@@ -96,7 +96,7 @@ func agnetRuntimeAdminRequest(t *testing.T, method string, url string, body stri
return resp
}
func readAgnetRuntimeSmokeBody(t *testing.T, resp *http.Response) string {
func readAgentRuntimeSmokeBody(t *testing.T, resp *http.Response) string {
t.Helper()
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
@@ -104,16 +104,16 @@ func readAgnetRuntimeSmokeBody(t *testing.T, resp *http.Response) string {
return string(body)
}
func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
db := setupAgnetRuntimeHTTPSmokeDB(t)
func TestAgentRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
db := setupAgentRuntimeHTTPSmokeDB(t)
runtimeCreateCalled := false
runtime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/agnet/health":
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/health":
require.Equal(t, "Bearer runtime-service-token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"healthy"}}`))
case r.Method == http.MethodPost && r.URL.Path == "/api/agnet/deployments":
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/sub-agile/deployments":
runtimeCreateCalled = true
require.Equal(t, "Bearer runtime-service-token", r.Header.Get("Authorization"))
require.Equal(t, "corr-http-smoke", r.Header.Get("X-Correlation-ID"))
@@ -142,23 +142,23 @@ func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
}))
defer runtime.Close()
t.Setenv("AGNET_RUNTIME_ENABLED", "true")
t.Setenv("AGNET_RUNTIME_ASYNC", "false")
t.Setenv("AGNET_RUNTIME_BASE_URL", runtime.URL)
t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "runtime-service-token")
t.Setenv("AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF", "azkv://heicode-kv.vault.azure.net/secrets/callback-signing")
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
t.Setenv("AGENT_RUNTIME_BASE_URL", runtime.URL)
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "runtime-service-token")
t.Setenv("AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF", "azkv://heicode-kv.vault.azure.net/secrets/callback-signing")
managerURL := startAgnetRuntimeManagerSmokeServer(t)
managerURL := startAgentRuntimeManagerSmokeServer(t)
healthResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/runtime/health", "")
healthBody := readAgnetRuntimeSmokeBody(t, healthResp)
healthResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/runtime/health", "")
healthBody := readAgentRuntimeSmokeBody(t, healthResp)
require.Equal(t, http.StatusOK, healthResp.StatusCode)
require.Contains(t, healthBody, `"success":true`)
require.Contains(t, healthBody, `"status":"healthy"`)
schemaResp, err := http.Get(managerURL + "/api/agnet/callbacks/swarm-events/schema")
schemaResp, err := http.Get(managerURL + "/api/agent/callbacks/runtime-events/schema")
require.NoError(t, err)
schemaBody := readAgnetRuntimeSmokeBody(t, schemaResp)
schemaBody := readAgentRuntimeSmokeBody(t, schemaResp)
require.Equal(t, http.StatusOK, schemaResp.StatusCode)
require.Contains(t, schemaBody, `"event_type":"task.claimed"`)
require.Contains(t, schemaBody, `"event_type":"approval.requested"`)
@@ -179,44 +179,44 @@ func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
"user_context":{"user_id":"101","channel_id":"default"},
"billing_context":{"provider":"newapi","newapi_user_ref":"newapi-http-smoke"},
"agile_context":{"iteration":"2026-05-27~2026-05-28","stage":"development","checkpoint":"ready_for_test","acceptance_criteria":["接口返回成功"],"next_action":"submit_test_result","requires_user_approval":false},
"agent_runtime":{"platform":"agnet","agents":[{"role":"builder","model_ref":"model-http-smoke","instance_count":1}]},
"agent_runtime":{"platform":"agent","agents":[{"role":"builder","model_ref":"model-http-smoke","instance_count":1}]},
"agents":[{"role_template":"builder","goal":"smoke","default_model_id":"model-http-smoke","resource_grants":[{"grant_id":"grant-http-git","resource_id":"git-http","resource_type":"git","user_id":"101","binding_scope":"https://example.invalid/heicode/smoke.git#main","target_role":"builder","target_agent_ref":"agent-builder-1","permission_scope":["repo:read"],"metadata":{"repo_url":"https://example.invalid/heicode/smoke.git"},"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/http-smoke-git","status":"active"}]}],
"constraints":{"allowed_model_ids":["model-http-smoke"]},
"metadata":{"correlation_id":"corr-http-smoke"}
}
}`
createResp := agnetRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agnet/deployments", createBody)
responseBody := readAgnetRuntimeSmokeBody(t, createResp)
createResp := agentRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agent/deployments", createBody)
responseBody := readAgentRuntimeSmokeBody(t, createResp)
require.Equal(t, http.StatusOK, createResp.StatusCode)
require.Contains(t, responseBody, `"success":true`)
require.Contains(t, responseBody, `"runtime_deployment_id":"runtime-http-dep"`)
require.Contains(t, responseBody, `"runtime_swarm_id":"runtime-http-swarm"`)
require.True(t, runtimeCreateCalled)
var stored model.AgnetDeployment
var stored model.AgentDeployment
require.NoError(t, db.Where("runtime_swarm_id = ?", "runtime-http-swarm").First(&stored).Error)
require.Equal(t, "accepted", stored.RuntimeState)
logsResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/deployments/"+stored.DeploymentID+"/logs", "")
logsBody := readAgnetRuntimeSmokeBody(t, logsResp)
logsResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/deployments/"+stored.DeploymentID+"/logs", "")
logsBody := readAgentRuntimeSmokeBody(t, logsResp)
require.Equal(t, http.StatusOK, logsResp.StatusCode)
require.Contains(t, logsBody, `"data_source":"manager_control_plane"`)
require.Contains(t, logsBody, `"runtime_source":"not_connected"`)
metricsResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/deployments/"+stored.DeploymentID+"/metrics", "")
metricsBody := readAgnetRuntimeSmokeBody(t, metricsResp)
metricsResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/deployments/"+stored.DeploymentID+"/metrics", "")
metricsBody := readAgentRuntimeSmokeBody(t, metricsResp)
require.Equal(t, http.StatusOK, metricsResp.StatusCode)
require.Contains(t, metricsBody, `"data_source":"manager_control_plane"`)
require.Contains(t, metricsBody, `"platform_estimated":true`)
}
func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
db := setupAgnetRuntimeHTTPSmokeDB(t)
db := setupAgentRuntimeHTTPSmokeDB(t)
swarmCreateCalled := false
swarmStopCalled := false
swarm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/agnet/health":
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/health":
require.Equal(t, "Bearer swarm-runtime-token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"healthy","service":"heicode-swarm-runtime"}}`))
@@ -253,16 +253,16 @@ func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
}))
defer swarm.Close()
t.Setenv("AGNET_RUNTIME_ENABLED", "false")
t.Setenv("AGENT_RUNTIME_ENABLED", "false")
t.Setenv("SWARM_RUNTIME_ENABLED", "true")
t.Setenv("SWARM_RUNTIME_ASYNC", "false")
t.Setenv("SWARM_RUNTIME_BASE_URL", swarm.URL)
t.Setenv("SWARM_RUNTIME_SERVICE_TOKEN", "swarm-runtime-token")
managerURL := startAgnetRuntimeManagerSmokeServer(t)
managerURL := startAgentRuntimeManagerSmokeServer(t)
healthResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/runtime/health?mode=swarm", "")
healthBody := readAgnetRuntimeSmokeBody(t, healthResp)
healthResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/runtime/health?mode=swarm", "")
healthBody := readAgentRuntimeSmokeBody(t, healthResp)
require.Equal(t, http.StatusOK, healthResp.StatusCode)
require.Contains(t, healthBody, `"mode":"swarm"`)
require.Contains(t, healthBody, `"status":"healthy"`)
@@ -279,7 +279,7 @@ func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
"user_context":{"user_id":"101","channel_id":"default"},
"billing_context":{"provider":"newapi","newapi_user_ref":"newapi-swarm-smoke"},
"agile_context":{"iteration":"2026-05-29","stage":"testing","checkpoint":"runtime_accepted","acceptance_criteria":["Runtime creates a swarm run"],"next_action":"submit_test_result","requires_user_approval":false},
"agent_runtime":{"platform":"agnet","agents":[{"role":"planner","model_ref":"model-swarm","instance_count":1},{"role":"builder","model_ref":"model-swarm","instance_count":1},{"role":"reviewer","model_ref":"model-swarm","instance_count":1}]},
"agent_runtime":{"platform":"agent","agents":[{"role":"planner","model_ref":"model-swarm","instance_count":1},{"role":"builder","model_ref":"model-swarm","instance_count":1},{"role":"reviewer","model_ref":"model-swarm","instance_count":1}]},
"agents":[
{"role_template":"planner","goal":"plan the swarm task","default_model_id":"model-swarm","resource_grants":[{"grant_id":"grant-swarm-plan","resource_id":"doc-swarm","resource_type":"project_doc","user_id":"101","binding_scope":"task-swarm-smoke","target_role":"planner","target_agent_ref":"agent-planner-1","permission_scope":["doc:read"],"metadata":{"resource_ref":"task-swarm-smoke"},"status":"active"}]},
{"role_template":"builder","goal":"build the swarm output","default_model_id":"model-swarm","resource_grants":[{"grant_id":"grant-swarm-build","resource_id":"git-swarm","resource_type":"git","user_id":"101","binding_scope":"task-swarm-smoke","target_role":"builder","target_agent_ref":"agent-builder-1","permission_scope":["repo:read"],"metadata":{"repo_url":"https://example.invalid/heicode/swarm.git"},"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/swarm-git","status":"active"}]},
@@ -289,21 +289,21 @@ func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
"metadata":{"correlation_id":"corr-swarm-smoke"}
}
}`
createResp := agnetRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/swarms", createBody)
responseBody := readAgnetRuntimeSmokeBody(t, createResp)
createResp := agentRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/swarms", createBody)
responseBody := readAgentRuntimeSmokeBody(t, createResp)
require.Equal(t, http.StatusOK, createResp.StatusCode)
require.Contains(t, responseBody, `"success":true`)
require.Contains(t, responseBody, `"runtime_deployment_id":"swarm-runtime-dep"`)
require.Contains(t, responseBody, `"runtime_swarm_id":"swarm-runtime-id"`)
require.True(t, swarmCreateCalled)
var stored model.AgnetDeployment
var stored model.AgentDeployment
require.NoError(t, db.Where("runtime_swarm_id = ?", "swarm-runtime-id").First(&stored).Error)
require.Equal(t, "agile", stored.SubMode)
require.Contains(t, stored.PlanJSON, `"runtime_mode":"swarm"`)
stopResp := agnetRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agnet/user/deployments/"+stored.DeploymentID+"/stop", `{"reason":"smoke done"}`)
stopBody := readAgnetRuntimeSmokeBody(t, stopResp)
stopResp := agentRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agent/user/deployments/"+stored.DeploymentID+"/stop", `{"reason":"smoke done"}`)
stopBody := readAgentRuntimeSmokeBody(t, stopResp)
require.Equal(t, http.StatusOK, stopResp.StatusCode)
require.Contains(t, stopBody, `"success":true`)
require.Contains(t, stopBody, `"runtime_state":"stopped"`)
+76 -44
View File
@@ -59,9 +59,11 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
apiRouter.GET("/agnet/callbacks/swarm-events/schema", controller.AgnetGetSwarmEventCallbackSchema)
apiRouter.POST("/agnet/callbacks/swarm-events", controller.AgnetReceiveSwarmEventCallback)
apiRouter.POST("/swarms", middleware.UserOrV2DeviceAuth(), controller.AgnetCreateUserSwarm)
apiRouter.GET("/agent/callbacks/runtime-events/schema", controller.AgentGetRuntimeEventCallbackSchema)
apiRouter.POST("/agent/callbacks/runtime-events", controller.AgentReceiveRuntimeEventCallback)
// Client-facing capability discovery (unified spec §6). Catalog data only.
apiRouter.GET("/heicode/capabilities", controller.HeicodeCapabilities)
apiRouter.POST("/swarms", middleware.UserOrV2DeviceAuth(), controller.AgentCreateUserSwarm)
//apiRouter.POST("/waffo-pancake/webhook", controller.WaffoPancakeWebhook)
// Universal secure verification routes
@@ -81,7 +83,7 @@ func SetApiRouter(router *gin.Engine) {
userRoute := apiRouter.Group("/user")
{
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
userRoute.POST("/session/from-agnet", middleware.CriticalRateLimit(), controller.HeicodeAgnetSessionLogin)
userRoute.POST("/session/from-agent", middleware.CriticalRateLimit(), controller.HeicodeAgentSessionLogin)
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), controller.Verify2FALogin)
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), controller.PasskeyLoginBegin)
@@ -491,50 +493,80 @@ func SetApiRouter(router *gin.Engine) {
deploymentsRoute.DELETE("/:id", controller.DeleteDeployment)
}
// Agnet user approval gates and short-lived credential leases.
agnetApprovalRoute := apiRouter.Group("/agnet")
agnetApprovalRoute.Use(middleware.UserOrV2DeviceAuth())
// Agent user approval gates and short-lived credential leases.
agentApprovalRoute := apiRouter.Group("/agent")
agentApprovalRoute.Use(middleware.UserOrV2DeviceAuth())
{
agnetApprovalRoute.GET("/approvals", controller.ListAgnetApprovalRequests)
agnetApprovalRoute.POST("/approvals", controller.CreateAgnetApprovalRequest)
agnetApprovalRoute.GET("/approvals/:approval_id", controller.GetAgnetApprovalRequest)
agnetApprovalRoute.POST("/approvals/:approval_id/approve", controller.ApproveAgnetApprovalRequest)
agnetApprovalRoute.POST("/approvals/:approval_id/reject", controller.RejectAgnetApprovalRequest)
agnetApprovalRoute.GET("/credential-leases", controller.ListAgnetCredentialLeases)
agnetApprovalRoute.POST("/credential-leases/:lease_id/revoke", controller.RevokeAgnetCredentialLease)
agnetApprovalRoute.GET("/role-templates", controller.AgnetListRoleTemplates)
agnetApprovalRoute.GET("/user/deployments", controller.AgnetListUserDeployments)
agnetApprovalRoute.POST("/user/deployments", controller.AgnetCreateUserDeployment)
agnetApprovalRoute.GET("/user/deployments/:deployment_id", controller.AgnetGetUserDeployment)
agnetApprovalRoute.POST("/user/deployments/:deployment_id/stop", controller.AgnetStopUserDeployment)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/logs", controller.AgnetListUserDeploymentLogs)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/metrics", controller.AgnetGetUserDeploymentMetrics)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/runtime-diagnostics", controller.AgnetGetUserDeploymentRuntimeDiagnostics)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/events", controller.AgnetListUserDeploymentEvents)
agnetApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgnetSimulateUserDeploymentEvents)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts", controller.AgnetListUserDeploymentArtifacts)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts/:artifact_id/content", controller.AgnetGetUserDeploymentArtifactContent)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/sk-snapshots", controller.AgnetListUserSKSnapshots)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/timeline", controller.AgnetGetUserDeploymentTimeline)
agnetApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgnetCreateTaskDeploymentDraft)
agentApprovalRoute.GET("/approvals", controller.ListAgentApprovalRequests)
agentApprovalRoute.POST("/approvals", controller.CreateAgentApprovalRequest)
agentApprovalRoute.GET("/approvals/:approval_id", controller.GetAgentApprovalRequest)
agentApprovalRoute.POST("/approvals/:approval_id/approve", controller.ApproveAgentApprovalRequest)
agentApprovalRoute.POST("/approvals/:approval_id/reject", controller.RejectAgentApprovalRequest)
agentApprovalRoute.GET("/credential-leases", controller.ListAgentCredentialLeases)
agentApprovalRoute.POST("/credential-leases/:lease_id/revoke", controller.RevokeAgentCredentialLease)
agentApprovalRoute.GET("/role-templates", controller.AgentListRoleTemplates)
agentApprovalRoute.GET("/user/deployments", controller.AgentListUserDeployments)
agentApprovalRoute.POST("/user/deployments", controller.AgentCreateUserDeployment)
agentApprovalRoute.GET("/user/deployments/:deployment_id", controller.AgentGetUserDeployment)
agentApprovalRoute.POST("/user/deployments/:deployment_id/stop", controller.AgentStopUserDeployment)
agentApprovalRoute.GET("/user/deployments/:deployment_id/logs", controller.AgentListUserDeploymentLogs)
agentApprovalRoute.GET("/user/deployments/:deployment_id/metrics", controller.AgentGetUserDeploymentMetrics)
agentApprovalRoute.GET("/user/deployments/:deployment_id/runtime-diagnostics", controller.AgentGetUserDeploymentRuntimeDiagnostics)
agentApprovalRoute.GET("/user/deployments/:deployment_id/events", controller.AgentListUserDeploymentEvents)
agentApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgentSimulateUserDeploymentEvents)
agentApprovalRoute.GET("/user/deployments/:deployment_id/artifacts", controller.AgentListUserDeploymentArtifacts)
agentApprovalRoute.GET("/user/deployments/:deployment_id/artifacts/:artifact_id/content", controller.AgentGetUserDeploymentArtifactContent)
agentApprovalRoute.GET("/user/deployments/:deployment_id/sk-snapshots", controller.AgentListUserSKSnapshots)
agentApprovalRoute.GET("/user/deployments/:deployment_id/timeline", controller.AgentGetUserDeploymentTimeline)
agentApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgentCreateTaskDeploymentDraft)
}
// Agnet orchestration control plane (minimal integration endpoints)
agnetRoute := apiRouter.Group("/agnet")
agnetRoute.Use(middleware.AdminAuth())
// Client-facing unified task routes (unified spec §5.1). task_id == the
// deployment_id, so these reuse the deployment control-plane handlers.
// Sub Agile -> agent_management; Swarm -> HeiCode-Swarm (mode is carried
// by the deployment record created at POST /tasks).
registerHeicodeTaskRoutes := func(group *gin.RouterGroup, createHandler gin.HandlerFunc) {
group.GET("/tasks", controller.AgentListUserDeployments)
group.POST("/tasks", createHandler)
group.GET("/tasks/:deployment_id", controller.AgentGetUserDeployment)
group.POST("/tasks/:deployment_id/stop", controller.AgentStopUserDeployment)
group.GET("/tasks/:deployment_id/timeline", controller.AgentGetUserDeploymentTimeline)
group.GET("/tasks/:deployment_id/workflow", controller.HeicodeTaskWorkflow)
group.GET("/tasks/:deployment_id/logs", controller.AgentListUserDeploymentLogs)
group.GET("/tasks/:deployment_id/events", controller.AgentListUserDeploymentEvents)
group.GET("/tasks/:deployment_id/metrics", controller.AgentGetUserDeploymentMetrics)
group.GET("/tasks/:deployment_id/diagnostics", controller.AgentGetUserDeploymentRuntimeDiagnostics)
group.GET("/tasks/:deployment_id/artifacts", controller.AgentListUserDeploymentArtifacts)
group.GET("/tasks/:deployment_id/artifacts/:artifact_id/content", controller.AgentGetUserDeploymentArtifactContent)
group.GET("/tasks/:deployment_id/sk-snapshots", controller.AgentListUserSKSnapshots)
group.POST("/tasks/:deployment_id/approvals/:approval_id/approve", controller.ApproveAgentApprovalRequest)
group.POST("/tasks/:deployment_id/approvals/:approval_id/reject", controller.RejectAgentApprovalRequest)
}
heicodeSubAgileRoute := apiRouter.Group("/heicode/sub-agile")
heicodeSubAgileRoute.Use(middleware.UserOrV2DeviceAuth())
registerHeicodeTaskRoutes(heicodeSubAgileRoute, controller.AgentCreateUserDeployment)
heicodeSwarmRoute := apiRouter.Group("/heicode/swarm")
heicodeSwarmRoute.Use(middleware.UserOrV2DeviceAuth())
registerHeicodeTaskRoutes(heicodeSwarmRoute, controller.AgentCreateUserSwarm)
// Agent orchestration control plane (minimal integration endpoints)
agentRoute := apiRouter.Group("/agent")
agentRoute.Use(middleware.AdminAuth())
{
agnetRoute.GET("/deployments", controller.AgnetListDeployments)
agnetRoute.POST("/deployments", controller.AgnetCreateDeployment)
agnetRoute.GET("/deployments/:deployment_id", controller.AgnetGetDeployment)
agnetRoute.POST("/deployments/:deployment_id/stop", controller.AgnetStopDeployment)
agnetRoute.GET("/deployments/:deployment_id/logs", controller.AgnetListDeploymentLogs)
agnetRoute.GET("/deployments/:deployment_id/metrics", controller.AgnetGetDeploymentMetrics)
agnetRoute.GET("/deployments/:deployment_id/events", controller.AgnetListDeploymentEvents)
agnetRoute.GET("/deployments/:deployment_id/sk-snapshots", controller.AgnetListSKSnapshots)
agnetRoute.POST("/sk-snapshots/resolve", controller.AgnetResolveSKSnapshots)
agnetRoute.GET("/runtime/health", controller.AgnetRuntimeHealth)
agnetRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgnetProjectDashboardSnapshot)
agnetRoute.GET("/audit-logs", controller.AgnetListAuditLogs)
agentRoute.GET("/deployments", controller.AgentListDeployments)
agentRoute.POST("/deployments", controller.AgentCreateDeployment)
agentRoute.GET("/deployments/:deployment_id", controller.AgentGetDeployment)
agentRoute.POST("/deployments/:deployment_id/stop", controller.AgentStopDeployment)
agentRoute.GET("/deployments/:deployment_id/logs", controller.AgentListDeploymentLogs)
agentRoute.GET("/deployments/:deployment_id/metrics", controller.AgentGetDeploymentMetrics)
agentRoute.GET("/deployments/:deployment_id/events", controller.AgentListDeploymentEvents)
agentRoute.GET("/deployments/:deployment_id/sk-snapshots", controller.AgentListSKSnapshots)
agentRoute.POST("/sk-snapshots/resolve", controller.AgentResolveSKSnapshots)
agentRoute.GET("/runtime/health", controller.AgentRuntimeHealth)
agentRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgentProjectDashboardSnapshot)
agentRoute.GET("/audit-logs", controller.AgentListAuditLogs)
}
}
}
+2 -2
View File
@@ -13,14 +13,14 @@
<meta name="title" content="Heicode Manager" />
<meta
name="description"
content="Heicode Manager — multi-tenant control plane for Agnet deployments, events and audit."
content="Heicode Manager — multi-tenant control plane for Agent deployments, events and audit."
/>
<meta property="og:title" content="Heicode Manager" />
<meta property="og:image" content="/logo.png?v=h-glass-2" />
<meta property="og:type" content="website" />
<meta name="theme-color" content="#7B6BE3" />
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.8fa3e0a349.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/9238.45c9c35ccf.js"></script><script defer src="/static/js/index.e57d82e6db.js"></script><link href="/static/css/index.fd51d44fe8.css" rel="stylesheet"></head>
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.829c7e3fad.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/6374.bc21d8b214.js"></script><script defer src="/static/js/index.99cba94710.js"></script><link href="/static/css/index.cc291c3921.css" rel="stylesheet"></head>
<body>
<div id="root"></div>
+1 -1
View File
@@ -13,7 +13,7 @@
<meta name="title" content="Heicode Manager" />
<meta
name="description"
content="Heicode Manager — multi-tenant control plane for Agnet deployments, events and audit."
content="Heicode Manager — multi-tenant control plane for Agent deployments, events and audit."
/>
<meta property="og:title" content="Heicode Manager" />
<meta property="og:image" content="/logo.png?v=h-glass-2" />
+1 -1
View File
@@ -105,7 +105,7 @@ export function QueryState(props: QueryStateProps) {
{loadingFallback ?? (
// Sensible default — three skeleton rows mimicking a card
// list. Callers pass `loadingFallback` to match their own
// grid (e.g. AgnetDeploymentsPage uses 2-col, audit uses
// grid (e.g. AgentDeploymentsPage uses 2-col, audit uses
// a vertical timeline).
<div className='space-y-3'>
<Skeleton className='h-24 w-full rounded-2xl' />
@@ -1,23 +1,23 @@
import { api } from '@/lib/api'
export type AgnetSubMode = 'agile' | 'waterfall'
export type AgentSubMode = 'agile' | 'waterfall'
/** Sub-agent cloud/runtime binding (passed to Agnet on deploy). */
export type AgnetRuntimeExecution = {
/** Sub-agent cloud/runtime binding (passed to Agent on deploy). */
export type AgentRuntimeExecution = {
profile_id?: string
cloud_principal_refs?: string[]
network_policy_ref?: string
}
/** SK allow/deny policy attached to the agent in the deployment plan. */
export type AgnetSKAccessPolicy = {
export type AgentSKAccessPolicy = {
policy_ref?: string
deny_skill_ids?: string[]
inherit_deployment_defaults?: boolean
}
/** Git-backed SK source (`type: git`). */
export type AgnetRepoRef = {
export type AgentRepoRef = {
connection_id?: string
repo_url?: string
ref: string
@@ -25,20 +25,20 @@ export type AgnetRepoRef = {
}
/** Single SK source entry (git or upload). */
export type AgnetSKSource = {
export type AgentSKSource = {
type?: string
artifact_id?: string
mime?: string
repo_ref?: AgnetRepoRef
repo_ref?: AgentRepoRef
}
export type AgnetAgentPlan = {
export type AgentAgentPlan = {
role_template: string
goal: string
default_model_id?: string
sk_sources?: AgnetSKSource[]
runtime_execution?: AgnetRuntimeExecution
sk_access_policy?: AgnetSKAccessPolicy
sk_sources?: AgentSKSource[]
runtime_execution?: AgentRuntimeExecution
sk_access_policy?: AgentSKAccessPolicy
resource_grants?: Array<{
grant_id?: string
resource_id?: string
@@ -56,13 +56,13 @@ export type AgnetAgentPlan = {
}>
}
export type AgnetBudget = {
export type AgentBudget = {
max_tokens: number
max_cost_usd: number
max_duration_sec: number
}
export type AgnetUserContext = {
export type AgentUserContext = {
user_id: string
email?: string
role?: string
@@ -70,15 +70,15 @@ export type AgnetUserContext = {
subscription_tier?: string
}
export type AgnetBillingContext = {
export type AgentBillingContext = {
provider?: 'newapi'
newapi_user_ref?: string
newapi_group?: string
quota_ref?: string
}
export type AgnetAgentRuntime = {
platform?: 'agnet'
export type AgentAgentRuntime = {
platform?: 'agent'
agents?: Array<{
role: string
model_ref: string
@@ -86,57 +86,57 @@ export type AgnetAgentRuntime = {
}>
}
export type AgnetConstraints = {
/** Runtime model allow-list for Agnet deployments; not a NewAPI billing map. */
export type AgentConstraints = {
/** Runtime model allow-list for Agent deployments; not a NewAPI billing map. */
allowed_model_ids?: string[]
}
export type AgnetOrchestrationMetadata = {
export type AgentOrchestrationMetadata = {
/**
* Compatibility field for Agnet routing scope; UI treats this as user scope,
* Compatibility field for Agent routing scope; UI treats this as user scope,
* not billing tenant.
*/
tenant_id: string
/**
* Compatibility field for Agnet routing scope; UI treats this as resource
* Compatibility field for Agent routing scope; UI treats this as resource
* scope, not project control.
*/
project_id: string
correlation_id: string
}
export type AgnetOrchestrationPlan = {
export type AgentOrchestrationPlan = {
intent_id: string
template_hint: string
objective: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
risk_level: 'low' | 'medium' | 'high'
budget: AgnetBudget
user_context: AgnetUserContext
billing_context?: AgnetBillingContext
agent_runtime?: AgnetAgentRuntime
agents: AgnetAgentPlan[]
constraints: AgnetConstraints
metadata: AgnetOrchestrationMetadata
budget: AgentBudget
user_context: AgentUserContext
billing_context?: AgentBillingContext
agent_runtime?: AgentAgentRuntime
agents: AgentAgentPlan[]
constraints: AgentConstraints
metadata: AgentOrchestrationMetadata
}
export type AgnetCreateDeploymentBody = {
orchestration_plan: AgnetOrchestrationPlan
export type AgentCreateDeploymentBody = {
orchestration_plan: AgentOrchestrationPlan
}
export type AgnetCreateDeploymentResult = {
export type AgentCreateDeploymentResult = {
deployment_id: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
status: string
agent_instances?: Array<{
instance_id?: string
role?: string
phase?: string
}>
permission_manifest?: AgnetPermissionManifest
permission_manifest?: AgentPermissionManifest
}
export type AgnetPermissionManifest = {
export type AgentPermissionManifest = {
user_id?: string
binding_scope?: string
agent_role?: string
@@ -153,9 +153,9 @@ export type AgnetPermissionManifest = {
}>
}
export type AgnetDeployment = {
export type AgentDeployment = {
deployment_id: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
status: string
phase: string
runtime_state?: string
@@ -165,24 +165,24 @@ export type AgnetDeployment = {
failure_reason?: string
created_at: string
updated_at: string
permission_manifest?: AgnetPermissionManifest
permission_manifest?: AgentPermissionManifest
orchestration_plan: {
intent_id?: string
template_hint?: string
objective?: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
risk_level?: string
budget?: AgnetBudget
agents?: AgnetAgentPlan[]
constraints?: AgnetConstraints
metadata?: AgnetOrchestrationMetadata
budget?: AgentBudget
agents?: AgentAgentPlan[]
constraints?: AgentConstraints
metadata?: AgentOrchestrationMetadata
}
}
export type AgnetRuntimeDiagnostics = {
export type AgentRuntimeDiagnostics = {
deployment_id: string
runtime_mode?: 'agnet' | 'swarm' | string
sub_mode?: AgnetSubMode
runtime_mode?: 'agent' | 'swarm' | string
sub_mode?: AgentSubMode
runtime_deployment_id?: string
runtime_swarm_id?: string
data_source?: string
@@ -198,7 +198,7 @@ export type AgnetRuntimeDiagnostics = {
checked_at?: string
}
export type AgnetApprovalRequest = {
export type AgentApprovalRequest = {
approval_id: string
user_id: number
deployment_id?: string
@@ -221,10 +221,10 @@ export type AgnetApprovalRequest = {
decided_at?: number
created_at?: number
updated_at?: number
credential_lease?: AgnetCredentialLease
credential_lease?: AgentCredentialLease
}
export type AgnetCredentialLease = {
export type AgentCredentialLease = {
lease_id: string
credential_ref: string
approval_id: string
@@ -273,10 +273,10 @@ export type GitSourcePayload = {
}
// Platform-recommended role catalog shape. Mirrors backend
// `AgnetRoleTemplate` in controller/agnet_role_template.go. The
// `AgentRoleTemplate` in controller/agent_role_template.go. The
// six canonical roles come from docs/product-package §13.3.3 —
// keys are stable identifiers, display strings can be translated.
export type AgnetRoleTemplate = {
export type AgentRoleTemplate = {
key: string
display_name: string
summary: string
@@ -288,12 +288,12 @@ export type AgnetRoleTemplate = {
// Cached at module level — the canonical six-role catalog doesn't
// change between page loads, so we avoid an extra request every
// time the create-deployment sheet opens.
let _roleTemplateCache: AgnetRoleTemplate[] | null = null
let _roleTemplateCache: AgentRoleTemplate[] | null = null
export async function listAgnetRoleTemplates(): Promise<AgnetRoleTemplate[]> {
export async function listAgentRoleTemplates(): Promise<AgentRoleTemplate[]> {
if (_roleTemplateCache) return _roleTemplateCache
const res = await api.get<ApiEnvelope<{ items?: AgnetRoleTemplate[] }>>(
'/api/agnet/role-templates'
const res = await api.get<ApiEnvelope<{ items?: AgentRoleTemplate[] }>>(
'/api/agent/role-templates'
)
const items = res.data?.data?.items ?? []
if (items.length > 0) {
@@ -302,16 +302,16 @@ export async function listAgnetRoleTemplates(): Promise<AgnetRoleTemplate[]> {
return items
}
export async function listAgnetDeployments(): Promise<AgnetDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
'/api/agnet/user/deployments'
export async function listAgentDeployments(): Promise<AgentDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentDeployment[] }>>(
'/api/agent/user/deployments'
)
return res.data?.data?.items ?? []
}
export async function listAgnetDeploymentsQuiet(): Promise<AgnetDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
'/api/agnet/user/deployments',
export async function listAgentDeploymentsQuiet(): Promise<AgentDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentDeployment[] }>>(
'/api/agent/user/deployments',
{
skipBusinessError: true,
skipErrorHandler: true,
@@ -321,11 +321,11 @@ export async function listAgnetDeploymentsQuiet(): Promise<AgnetDeployment[]> {
return res.data?.data?.items ?? []
}
export async function createAgnetDeployment(
body: AgnetCreateDeploymentBody
): Promise<AgnetCreateDeploymentResult> {
const res = await api.post<ApiEnvelope<AgnetCreateDeploymentResult>>(
'/api/agnet/user/deployments',
export async function createAgentDeployment(
body: AgentCreateDeploymentBody
): Promise<AgentCreateDeploymentResult> {
const res = await api.post<ApiEnvelope<AgentCreateDeploymentResult>>(
'/api/agent/user/deployments',
body
)
const env = res.data
@@ -339,28 +339,28 @@ export async function createAgnetDeployment(
return data
}
export async function getAgnetDeploymentEvents(deploymentId: string) {
export async function getAgentDeploymentEvents(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/user/deployments/${deploymentId}/events`)
>(`/api/agent/user/deployments/${deploymentId}/events`)
return res.data?.data?.items ?? []
}
export async function getAgnetDeploymentArtifacts(deploymentId: string) {
export async function getAgentDeploymentArtifacts(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/user/deployments/${deploymentId}/artifacts`)
>(`/api/agent/user/deployments/${deploymentId}/artifacts`)
return res.data?.data?.items ?? []
}
export async function getAgnetDeploymentSKSnapshots(deploymentId: string) {
export async function getAgentDeploymentSKSnapshots(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/user/deployments/${deploymentId}/sk-snapshots`)
>(`/api/agent/user/deployments/${deploymentId}/sk-snapshots`)
return res.data?.data?.items ?? []
}
export async function getAgnetDeploymentTimeline(deploymentId: string) {
export async function getAgentDeploymentTimeline(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{
callbacks?: Array<Record<string, unknown>>
@@ -368,7 +368,7 @@ export async function getAgnetDeploymentTimeline(deploymentId: string) {
sk_snapshots?: Array<Record<string, unknown>>
timeline?: Array<Record<string, unknown>>
}>
>(`/api/agnet/user/deployments/${deploymentId}/timeline`)
>(`/api/agent/user/deployments/${deploymentId}/timeline`)
return (
res.data?.data ?? {
callbacks: [],
@@ -379,11 +379,11 @@ export async function getAgnetDeploymentTimeline(deploymentId: string) {
)
}
export async function getAgnetRuntimeDiagnostics(
export async function getAgentRuntimeDiagnostics(
deploymentId: string
): Promise<AgnetRuntimeDiagnostics | null> {
const res = await api.get<ApiEnvelope<AgnetRuntimeDiagnostics>>(
`/api/agnet/user/deployments/${deploymentId}/runtime-diagnostics`,
): Promise<AgentRuntimeDiagnostics | null> {
const res = await api.get<ApiEnvelope<AgentRuntimeDiagnostics>>(
`/api/agent/user/deployments/${deploymentId}/runtime-diagnostics`,
{
skipBusinessError: true,
skipErrorHandler: true,
@@ -393,46 +393,46 @@ export async function getAgnetRuntimeDiagnostics(
return res.data?.data ?? null
}
export async function simulateAgnetDeploymentEvents(
export async function simulateAgentDeploymentEvents(
deploymentId: string,
events?: string[]
): Promise<{ deployment_id: string; simulated: boolean; total: number }> {
const res = await api.post<
ApiEnvelope<{ deployment_id: string; simulated: boolean; total: number }>
>(`/api/agnet/user/deployments/${deploymentId}/simulate-events`, {
>(`/api/agent/user/deployments/${deploymentId}/simulate-events`, {
events: events ?? [],
})
const env = res.data
if (!env?.success || !env.data) {
throw new Error(env?.message || 'simulateAgnetDeploymentEvents failed')
throw new Error(env?.message || 'simulateAgentDeploymentEvents failed')
}
return env.data
}
export async function getAgnetAuditLogs() {
export async function getAgentAuditLogs() {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>('/api/agnet/audit-logs')
>('/api/agent/audit-logs')
return res.data?.data?.items ?? []
}
export async function listAgnetApprovals(params?: {
export async function listAgentApprovals(params?: {
status?: string
deployment_id?: string
}): Promise<AgnetApprovalRequest[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetApprovalRequest[] }>>(
'/api/agnet/approvals',
}): Promise<AgentApprovalRequest[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentApprovalRequest[] }>>(
'/api/agent/approvals',
{ params }
)
return res.data?.data?.items ?? []
}
export async function approveAgnetApproval(
export async function approveAgentApproval(
approvalId: string,
reason?: string
): Promise<AgnetApprovalRequest> {
const res = await api.post<ApiEnvelope<AgnetApprovalRequest>>(
`/api/agnet/approvals/${approvalId}/approve`,
): Promise<AgentApprovalRequest> {
const res = await api.post<ApiEnvelope<AgentApprovalRequest>>(
`/api/agent/approvals/${approvalId}/approve`,
{ reason }
)
if (!res.data?.success || !res.data.data) {
@@ -441,12 +441,12 @@ export async function approveAgnetApproval(
return res.data.data
}
export async function rejectAgnetApproval(
export async function rejectAgentApproval(
approvalId: string,
reason?: string
): Promise<AgnetApprovalRequest> {
const res = await api.post<ApiEnvelope<AgnetApprovalRequest>>(
`/api/agnet/approvals/${approvalId}/reject`,
): Promise<AgentApprovalRequest> {
const res = await api.post<ApiEnvelope<AgentApprovalRequest>>(
`/api/agent/approvals/${approvalId}/reject`,
{ reason }
)
if (!res.data?.success || !res.data.data) {
@@ -455,23 +455,23 @@ export async function rejectAgnetApproval(
return res.data.data
}
export async function listAgnetCredentialLeases(params?: {
export async function listAgentCredentialLeases(params?: {
status?: string
deployment_id?: string
}): Promise<AgnetCredentialLease[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetCredentialLease[] }>>(
'/api/agnet/credential-leases',
}): Promise<AgentCredentialLease[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentCredentialLease[] }>>(
'/api/agent/credential-leases',
{ params }
)
return res.data?.data?.items ?? []
}
export async function revokeAgnetCredentialLease(
export async function revokeAgentCredentialLease(
leaseId: string,
reason?: string
): Promise<AgnetCredentialLease> {
const res = await api.post<ApiEnvelope<AgnetCredentialLease>>(
`/api/agnet/credential-leases/${leaseId}/revoke`,
): Promise<AgentCredentialLease> {
const res = await api.post<ApiEnvelope<AgentCredentialLease>>(
`/api/agent/credential-leases/${leaseId}/revoke`,
{ reason }
)
if (!res.data?.success || !res.data.data) {
@@ -480,10 +480,10 @@ export async function revokeAgnetCredentialLease(
return res.data.data
}
export async function getAgnetSnapshots(deploymentId: string) {
export async function getAgentSnapshots(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/deployments/${deploymentId}/sk-snapshots`, {
>(`/api/agent/deployments/${deploymentId}/sk-snapshots`, {
skipBusinessError: true,
skipErrorHandler: true,
} as Record<string, unknown>)
@@ -40,13 +40,13 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import {
createAgnetDeployment,
listAgnetRoleTemplates,
type AgnetAgentPlan,
type AgnetOrchestrationPlan,
type AgnetRoleTemplate,
type AgnetSKSource,
type AgnetSubMode,
createAgentDeployment,
listAgentRoleTemplates,
type AgentAgentPlan,
type AgentOrchestrationPlan,
type AgentRoleTemplate,
type AgentSKSource,
type AgentSubMode,
} from './api'
type ResourceType =
@@ -240,7 +240,7 @@ function formatRiskLevel(
return t('High')
}
function formatSubMode(value: AgnetSubMode, t: (key: string) => string) {
function formatSubMode(value: AgentSubMode, t: (key: string) => string) {
if (value === 'agile') return t('Agile')
return t('Waterfall')
}
@@ -261,11 +261,11 @@ function formatRoleLabel(value: string, t: (key: string) => string) {
function formatRoleDisplayName(value: string, t: (key: string) => string) {
const name = value.trim().toLowerCase()
const labels: Record<string, string> = {
'product agnet': t('Product Agnet'),
'frontend agnet': t('Frontend Agnet'),
'backend agnet': t('Backend Agnet'),
'reviewer agnet': t('Reviewer Agnet'),
'ops agnet': t('Ops Agnet'),
'product agent': t('Product Agent'),
'frontend agent': t('Frontend Agent'),
'backend agent': t('Backend Agent'),
'reviewer agent': t('Reviewer Agent'),
'ops agent': t('Ops Agent'),
}
return labels[name] || value
}
@@ -293,15 +293,15 @@ function buildAgent(
resourceScopeRef: string
correlationId: string
}
): AgnetAgentPlan {
let sk_sources: AgnetSKSource[] | undefined
): AgentAgentPlan {
let sk_sources: AgentSKSource[] | undefined
const raw = row.sk_sources_json.trim()
if (raw) {
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) {
throw new Error('sk_sources_must_be_array')
}
sk_sources = parsed as AgnetSKSource[]
sk_sources = parsed as AgentSKSource[]
}
const role = row.role_template.trim()
@@ -404,7 +404,7 @@ function SectionTitle({
)
}
export function CreateAgnetDeploymentSheet({
export function CreateAgentDeploymentSheet({
open,
onOpenChange,
}: {
@@ -419,7 +419,7 @@ export function CreateAgnetDeploymentSheet({
const [step, setStep] = useState('idea')
const [templateHint, setTemplateHint] = useState(defaultTemplate.id)
const [objective, setObjective] = useState(defaultTemplate.objective)
const [subMode, setSubMode] = useState<AgnetSubMode>('agile')
const [subMode, setSubMode] = useState<AgentSubMode>('agile')
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high'>(
defaultTemplate.risk
)
@@ -442,11 +442,11 @@ export function CreateAgnetDeploymentSheet({
// subsequent sheet opens don't re-fetch. Empty array fallback
// means the sheet stays usable if the catalog endpoint is down —
// the role picker falls back to free-text input.
const [roleTemplates, setRoleTemplates] = useState<AgnetRoleTemplate[]>([])
const [roleTemplates, setRoleTemplates] = useState<AgentRoleTemplate[]>([])
useEffect(() => {
if (!open) return
let cancelled = false
listAgnetRoleTemplates()
listAgentRoleTemplates()
.then((items) => {
if (!cancelled) setRoleTemplates(items)
})
@@ -556,7 +556,7 @@ export function CreateAgnetDeploymentSheet({
risk_level: riskLevel,
resource_scope_ref: resourceScopeRef.trim(),
agent_runtime: {
platform: 'agnet',
platform: 'agent',
agents: previewAgents
.filter((agent) => agent.role_template && agent.default_model_id)
.map((agent) => ({
@@ -624,7 +624,7 @@ export function CreateAgnetDeploymentSheet({
mutationFn: async () => {
const intentId = crypto.randomUUID()
const allowed = splitComma(allowedModels)
let agentPlans: AgnetAgentPlan[]
let agentPlans: AgentAgentPlan[]
try {
agentPlans = agents.map((row, index) =>
buildAgent(row, index, {
@@ -648,7 +648,7 @@ export function CreateAgnetDeploymentSheet({
instance_count: 1,
}))
const plan: AgnetOrchestrationPlan = {
const plan: AgentOrchestrationPlan = {
intent_id: intentId,
template_hint: templateHint.trim(),
objective: objective.trim(),
@@ -674,7 +674,7 @@ export function CreateAgnetDeploymentSheet({
agent_runtime:
runtimeAgents.length > 0
? {
platform: 'agnet',
platform: 'agent',
agents: runtimeAgents,
}
: undefined,
@@ -689,15 +689,15 @@ export function CreateAgnetDeploymentSheet({
},
}
return createAgnetDeployment({ orchestration_plan: plan })
return createAgentDeployment({ orchestration_plan: plan })
},
onSuccess: (data) => {
toast.success(
t('Agnet deployment created', {
t('Agent deployment created', {
deployment_id: data.deployment_id,
}) as string
)
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'deployments'] })
onOpenChange(false)
resetForm()
},
@@ -850,7 +850,7 @@ export function CreateAgnetDeploymentSheet({
<Label>{t('Sub mode')}</Label>
<Select
value={subMode}
onValueChange={(v) => setSubMode(v as AgnetSubMode)}
onValueChange={(v) => setSubMode(v as AgentSubMode)}
>
<SelectTrigger className='h-9'>
<SelectValue />
@@ -896,7 +896,7 @@ export function CreateAgnetDeploymentSheet({
/>
</div>
<div className='grid gap-2'>
<Label>{t('Agnet allowed models comma')}</Label>
<Label>{t('Agent allowed models comma')}</Label>
<Input
value={allowedModels}
onChange={(e) => setAllowedModels(e.target.value)}
@@ -952,7 +952,7 @@ export function CreateAgnetDeploymentSheet({
icon={Bot}
title={t('Role cards')}
hint={t(
'Each card maps one child Agnet role to a runtime model and one bounded resource grant.'
'Each card maps one child Agent role to a runtime model and one bounded resource grant.'
)}
/>
<Button
@@ -1002,7 +1002,7 @@ export function CreateAgnetDeploymentSheet({
<div className='grid gap-3'>
<div className='grid gap-2 sm:grid-cols-3'>
{/* Role picker — bound to the canonical six-role
catalog from /api/agnet/role-templates. Falls
catalog from /api/agent/role-templates. Falls
back to a free-text input if the catalog
failed to load. */}
{roleTemplates.length > 0 ? (
@@ -1039,7 +1039,7 @@ export function CreateAgnetDeploymentSheet({
/>
)}
<Input
placeholder={t('Agnet runtime model id')}
placeholder={t('Agent runtime model id')}
value={agent.default_model_id}
onChange={(e) =>
updateAgent(index, {
@@ -1336,7 +1336,7 @@ export function CreateAgnetDeploymentSheet({
0 ? (
<p className='text-muted-foreground mt-2 text-sm italic'>
{t(
'No resources bound yet — Agnet will run with no external data access.'
'No resources bound yet — Agent will run with no external data access.'
)}
</p>
) : (
@@ -61,25 +61,25 @@ import {
import { Skeleton } from '@/components/ui/skeleton'
import { QueryState } from '@/components/query-state'
import {
approveAgnetApproval,
getAgnetDeploymentEvents,
getAgnetRuntimeDiagnostics,
getAgnetDeploymentTimeline,
listAgnetApprovals,
listAgnetCredentialLeases,
listAgnetDeployments,
rejectAgnetApproval,
revokeAgnetCredentialLease,
simulateAgnetDeploymentEvents,
type AgnetApprovalRequest,
type AgnetCredentialLease,
type AgnetDeployment,
type AgnetRuntimeDiagnostics,
type AgnetRuntimeExecution,
type AgnetSKAccessPolicy,
approveAgentApproval,
getAgentDeploymentEvents,
getAgentRuntimeDiagnostics,
getAgentDeploymentTimeline,
listAgentApprovals,
listAgentCredentialLeases,
listAgentDeployments,
rejectAgentApproval,
revokeAgentCredentialLease,
simulateAgentDeploymentEvents,
type AgentApprovalRequest,
type AgentCredentialLease,
type AgentDeployment,
type AgentRuntimeDiagnostics,
type AgentRuntimeExecution,
type AgentSKAccessPolicy,
} from './api'
import { AzureCloudBindingSheet } from './azure-cloud-binding-sheet'
import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
import { CreateAgentDeploymentSheet } from './create-agent-deployment-sheet'
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
@@ -260,7 +260,7 @@ function MetaPill({
)
}
function describeRiskLevel(dep: AgnetDeployment): {
function describeRiskLevel(dep: AgentDeployment): {
label: string
tone: 'low' | 'mid' | 'high'
} {
@@ -288,7 +288,7 @@ function formatRiskLabel(label: string, t: (key: string) => string): string {
return label
}
function describeSubMode(dep: AgnetDeployment): string {
function describeSubMode(dep: AgentDeployment): string {
return dep.sub_mode || dep.orchestration_plan?.sub_mode || 'agile'
}
@@ -299,7 +299,7 @@ function formatSubModeLabel(mode: string, t: (key: string) => string): string {
return mode
}
function describeBudget(dep: AgnetDeployment): string {
function describeBudget(dep: AgentDeployment): string {
const budget = dep.orchestration_plan?.budget
if (!budget) {
const agents = dep.orchestration_plan?.agents?.length ?? 0
@@ -314,7 +314,7 @@ function describeBudget(dep: AgnetDeployment): string {
return parts.length > 0 ? parts.join(' / ') : '—'
}
function describeScope(dep: AgnetDeployment): string {
function describeScope(dep: AgentDeployment): string {
const firstGrant = dep.orchestration_plan?.agents?.flatMap(
(agent) => agent.resource_grants || []
)[0]
@@ -452,7 +452,7 @@ function buildTaskFlowSummaries(
}
function collectResourceGrants(
dep: AgnetDeployment
dep: AgentDeployment
): Record<string, unknown>[] {
const manifestGrants = dep.permission_manifest?.resource_grants
if (manifestGrants && manifestGrants.length > 0) {
@@ -468,7 +468,7 @@ function collectResourceGrants(
)
}
function describeSecretRefs(dep: AgnetDeployment): string {
function describeSecretRefs(dep: AgentDeployment): string {
const grants = collectResourceGrants(dep)
const refs = grants.filter((grant) => {
const secretRef = grant.secret_ref
@@ -568,17 +568,17 @@ function runtimeWarningLabel(
}
function runtimeModeLabel(
diagnostics: AgnetRuntimeDiagnostics | null | undefined,
diagnostics: AgentRuntimeDiagnostics | null | undefined,
t: (key: string) => string
): string {
const mode = String(diagnostics?.runtime_mode || '').toLowerCase()
if (mode === 'swarm') return t('Swarm mode')
if (mode === 'agnet') return t('Ordinary sub mode')
if (mode === 'agent') return t('Ordinary sub mode')
return mode || '—'
}
function runtimeAgentRows(
diagnostics: AgnetRuntimeDiagnostics | null | undefined
diagnostics: AgentRuntimeDiagnostics | null | undefined
) {
return (diagnostics?.agents ?? []).slice(0, 4).map((agent, idx) => ({
id: String(agent.agent_id || agent.instance_id || idx),
@@ -592,7 +592,7 @@ function RuntimeDiagnosticsPanel({
diagnostics,
isLoading,
}: {
diagnostics?: AgnetRuntimeDiagnostics | null
diagnostics?: AgentRuntimeDiagnostics | null
isLoading: boolean
}) {
const { t } = useTranslation()
@@ -685,7 +685,7 @@ function RuntimeDiagnosticsPanel({
)
}
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
function RunDetailPanel({ dep }: { dep: AgentDeployment }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const currentUser = useAuthStore((state) => state.auth.user)
@@ -694,21 +694,21 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
const risk = describeRiskLevel(dep)
const grants = collectResourceGrants(dep)
const runtimeDiagnosticsQuery = useQuery({
queryKey: ['agnet', 'runtime-diagnostics', dep.deployment_id],
queryFn: () => getAgnetRuntimeDiagnostics(dep.deployment_id),
queryKey: ['agent', 'runtime-diagnostics', dep.deployment_id],
queryFn: () => getAgentRuntimeDiagnostics(dep.deployment_id),
enabled: Boolean(dep.deployment_id),
refetchInterval: 30_000,
})
const runtimeDiagnostics = runtimeDiagnosticsQuery.data
const simulateMutation = useMutation({
mutationFn: () => simulateAgnetDeploymentEvents(dep.deployment_id),
mutationFn: () => simulateAgentDeploymentEvents(dep.deployment_id),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'deployments'] })
void queryClient.invalidateQueries({
queryKey: ['agnet', 'deployment-events', dep.deployment_id],
queryKey: ['agent', 'deployment-events', dep.deployment_id],
})
void queryClient.invalidateQueries({
queryKey: ['agnet', 'deployment-timeline', dep.deployment_id],
queryKey: ['agent', 'deployment-timeline', dep.deployment_id],
})
toast.success(t('Simulated events recorded'))
},
@@ -986,7 +986,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
// Deployments page
// =============================================================================
export function AgnetDeploymentsPage() {
export function AgentDeploymentsPage() {
const { t } = useTranslation()
const [filter, setFilter] = useState<'all' | StatusKey>('all')
const [keyword, setKeyword] = useState('')
@@ -999,8 +999,8 @@ export function AgnetDeploymentsPage() {
error: deploymentsError,
refetch: refetchDeployments,
} = useQuery({
queryKey: ['agnet', 'deployments'],
queryFn: listAgnetDeployments,
queryKey: ['agent', 'deployments'],
queryFn: listAgentDeployments,
refetchInterval: 30_000,
retry: false, // QueryState handles error display; no silent retries
})
@@ -1034,7 +1034,7 @@ export function AgnetDeploymentsPage() {
<PageSurface
title={t('Task overview')}
subtitle={t(
'Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.'
'Status, latest activity and last update for every Agent task you launched. Details live in the desktop client.'
)}
toolbar={
<>
@@ -1143,7 +1143,7 @@ export function AgnetDeploymentsPage() {
</div>
</QueryState>
</PageSurface>
<CreateAgnetDeploymentSheet
<CreateAgentDeploymentSheet
open={createOpen}
onOpenChange={setCreateOpen}
/>
@@ -1199,8 +1199,8 @@ export function AgnetDeploymentsPage() {
function RunAuditTimeline({ deploymentId }: { deploymentId: string }) {
const { t } = useTranslation()
const { data = [], isLoading } = useQuery({
queryKey: ['agnet', 'deployment-events', deploymentId],
queryFn: () => getAgnetDeploymentEvents(deploymentId),
queryKey: ['agent', 'deployment-events', deploymentId],
queryFn: () => getAgentDeploymentEvents(deploymentId),
enabled: Boolean(deploymentId),
refetchInterval: 15_000,
})
@@ -1286,8 +1286,8 @@ function RunAuditTimeline({ deploymentId }: { deploymentId: string }) {
function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
const { t } = useTranslation()
const { data, isLoading } = useQuery({
queryKey: ['agnet', 'deployment-timeline', deploymentId],
queryFn: () => getAgnetDeploymentTimeline(deploymentId),
queryKey: ['agent', 'deployment-timeline', deploymentId],
queryFn: () => getAgentDeploymentTimeline(deploymentId),
enabled: Boolean(deploymentId),
refetchInterval: 15_000,
})
@@ -1490,7 +1490,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
)}
{Boolean(item.artifact_id) && (
<a
href={`/api/agnet/user/deployments/${encodeURIComponent(deploymentId)}/artifacts/${encodeURIComponent(String(item.artifact_id))}/content`}
href={`/api/agent/user/deployments/${encodeURIComponent(deploymentId)}/artifacts/${encodeURIComponent(String(item.artifact_id))}/content`}
target='_blank'
rel='noreferrer'
className='text-primary mt-2 inline-flex items-center gap-1 text-[11px] font-medium hover:underline'
@@ -1606,13 +1606,13 @@ function classifyEventLevel(entry: Record<string, unknown>): EventLevel {
return 'info'
}
export function AgnetEventsPage() {
export function AgentEventsPage() {
const { t } = useTranslation()
const [level, setLevel] = useState<EventLevel>('all')
const deploymentsQuery = useQuery({
queryKey: ['agnet', 'deployments'],
queryFn: listAgnetDeployments,
queryKey: ['agent', 'deployments'],
queryFn: listAgentDeployments,
})
const deployments = deploymentsQuery.data ?? []
const [activeDeployment, setActiveDeployment] = useState<string | undefined>(
@@ -1621,8 +1621,8 @@ export function AgnetEventsPage() {
const effectiveDeployment = activeDeployment ?? deployments[0]?.deployment_id
const eventsQuery = useQuery({
queryKey: ['agnet', 'events', effectiveDeployment],
queryFn: () => getAgnetDeploymentEvents(effectiveDeployment as string),
queryKey: ['agent', 'events', effectiveDeployment],
queryFn: () => getAgentDeploymentEvents(effectiveDeployment as string),
enabled: Boolean(effectiveDeployment),
})
@@ -1636,7 +1636,7 @@ export function AgnetEventsPage() {
<PageSurface
title={t('Events')}
subtitle={t(
'Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.'
'Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.'
)}
toolbar={
<>
@@ -1919,14 +1919,14 @@ function formatUnixMs(value?: number) {
return new Date(value).toLocaleString()
}
function AgnetApprovalCard({
function AgentApprovalCard({
approval,
approveBusy,
rejectBusy,
onApprove,
onReject,
}: {
approval: AgnetApprovalRequest
approval: AgentApprovalRequest
approveBusy: boolean
rejectBusy: boolean
onApprove: () => void
@@ -1977,12 +1977,12 @@ function AgnetApprovalCard({
)
}
function AgnetLeaseCard({
function AgentLeaseCard({
lease,
busy,
onRevoke,
}: {
lease: AgnetCredentialLease
lease: AgentCredentialLease
busy: boolean
onRevoke: () => void
}) {
@@ -2013,7 +2013,7 @@ function AgnetLeaseCard({
)
}
export function AgnetAuditPage() {
export function AgentAuditPage() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [scope, setScope] = useState('')
@@ -2021,30 +2021,30 @@ export function AgnetAuditPage() {
const [actionFilter, setActionFilter] = useState('')
const approvalsQuery = useQuery({
queryKey: ['agnet', 'approvals', 'pending'],
queryFn: () => listAgnetApprovals({ status: 'pending' }),
queryKey: ['agent', 'approvals', 'pending'],
queryFn: () => listAgentApprovals({ status: 'pending' }),
refetchInterval: 30_000,
})
const leasesQuery = useQuery({
queryKey: ['agnet', 'credential-leases', 'active'],
queryFn: () => listAgnetCredentialLeases({ status: 'active' }),
queryKey: ['agent', 'credential-leases', 'active'],
queryFn: () => listAgentCredentialLeases({ status: 'active' }),
refetchInterval: 30_000,
})
const refreshApprovalState = () => {
void queryClient.invalidateQueries({ queryKey: ['agnet', 'approvals'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'approvals'] })
void queryClient.invalidateQueries({
queryKey: ['agnet', 'credential-leases'],
queryKey: ['agent', 'credential-leases'],
})
void queryClient.invalidateQueries({
queryKey: ['heicode', 'agnet', 'audit'],
queryKey: ['heicode', 'agent', 'audit'],
})
}
const approveMutation = useMutation({
mutationFn: (approvalId: string) =>
approveAgnetApproval(approvalId, t('Approved from Manager audit page')),
approveAgentApproval(approvalId, t('Approved from Manager audit page')),
onSuccess: () => {
toast.success(t('Approval accepted'))
refreshApprovalState()
@@ -2053,7 +2053,7 @@ export function AgnetAuditPage() {
const rejectMutation = useMutation({
mutationFn: (approvalId: string) =>
rejectAgnetApproval(approvalId, t('Rejected from Manager audit page')),
rejectAgentApproval(approvalId, t('Rejected from Manager audit page')),
onSuccess: () => {
toast.success(t('Approval rejected'))
refreshApprovalState()
@@ -2062,7 +2062,7 @@ export function AgnetAuditPage() {
const revokeMutation = useMutation({
mutationFn: (leaseId: string) =>
revokeAgnetCredentialLease(leaseId, t('Revoked from Manager audit page')),
revokeAgentCredentialLease(leaseId, t('Revoked from Manager audit page')),
onSuccess: () => {
toast.success(t('Credential lease revoked'))
refreshApprovalState()
@@ -2075,7 +2075,7 @@ export function AgnetAuditPage() {
error: auditError,
refetch: refetchAudit,
} = useQuery({
queryKey: ['heicode', 'agnet', 'audit'],
queryKey: ['heicode', 'agent', 'audit'],
queryFn: () => listMcpAuditLogs({ limit: 200 }),
refetchInterval: 60_000,
retry: false,
@@ -2139,7 +2139,7 @@ export function AgnetAuditPage() {
</h3>
<p className='text-muted-foreground text-xs'>
{t(
'Approve or reject high-risk Agnet operations before credentials are leased.'
'Approve or reject high-risk Agent operations before credentials are leased.'
)}
</p>
</div>
@@ -2156,7 +2156,7 @@ export function AgnetAuditPage() {
>
<div className='grid gap-3'>
{(approvalsQuery.data ?? []).map((approval) => (
<AgnetApprovalCard
<AgentApprovalCard
key={approval.approval_id}
approval={approval}
approveBusy={approveMutation.isPending}
@@ -2193,7 +2193,7 @@ export function AgnetAuditPage() {
>
<div className='grid gap-3'>
{(leasesQuery.data ?? []).map((lease) => (
<AgnetLeaseCard
<AgentLeaseCard
key={lease.lease_id}
lease={lease}
busy={revokeMutation.isPending}
@@ -2232,12 +2232,12 @@ export function AgnetAuditPage() {
// resource-binding slice for Work/Runs.
// =============================================================================
// AgnetSKSourcesPage — “准备清单” wizard. Frames the page as a 4-step list
// AgentSKSourcesPage — “准备清单” wizard. Frames the page as a 4-step list
// (代码 / 文档 / 云账号 / 推荐摘要) per docs/product-package/10 §"Manager 准备清单"
// + /11 §5. Does not expose repo_url / ref / paths / usage / tenant_id as the
// main flow — those move into a “手动补充”次级 sheet only opened when the user
// clicks “连接代码仓库 → 高级补充”.
// AgnetSKSourcesPage — 准备清单 wizard.
// AgentSKSourcesPage — 准备清单 wizard.
//
// Data layer switched (commit ?) from the Heicode-local git_sources controller
// to mcp-server §2 ResourceBinding (/api/resources) per the contract docs
@@ -2258,7 +2258,7 @@ export function AgnetAuditPage() {
// (none) → secret_ref (Azure Key Vault azkv://...
// reference when the resource has
// credential material)
export function AgnetSKSourcesPage() {
export function AgentSKSourcesPage() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [advancedOpen, setAdvancedOpen] = useState(false)
@@ -2406,7 +2406,7 @@ export function AgnetSKSourcesPage() {
docSources.length > 0
? t('{{n}} doc source connected', { n: docSources.length })
: t(
'Link product requirements, design docs or wiki repos so Agnet has project context.'
'Link product requirements, design docs or wiki repos so Agent has project context.'
),
done: docSources.length > 0,
optional: true,
@@ -2457,7 +2457,7 @@ export function AgnetSKSourcesPage() {
<PageSurface
title={t('Preparation checklist')}
subtitle={t(
'Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agnet.'
'Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agent.'
)}
toolbar={
<span className='text-primary inline-flex items-center gap-1.5 rounded-full border border-[color-mix(in_oklch,var(--primary)_30%,var(--border))] bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-3 py-1 text-[11px] font-semibold tracking-[0.12em] uppercase'>
@@ -2524,7 +2524,7 @@ export function AgnetSKSourcesPage() {
disabled={!prereqsDone}
onClick={() => setSummaryOpen(true)}
>
{t('Confirm and launch Agnet')}
{t('Confirm and launch Agent')}
</Button>
) : step.key === 'cloud' ? (
// Open the Azure-specific sheet. Manager stores the secret in
@@ -2558,7 +2558,7 @@ export function AgnetSKSourcesPage() {
<p className='text-foreground font-medium'>{t('How this works')}</p>
<p className='mt-2'>
{t(
'Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.'
'Long-lived credentials are stored in the secret vault. Agent only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.'
)}
</p>
</div>
@@ -2812,8 +2812,8 @@ export function AgnetSKSourcesPage() {
// =============================================================================
// 推荐确认卡 — docs/product-package/10 §"推荐确认卡":
// 本次会做 / 本次允许使用 / 本次不会做 / 高危规则 / 预计消耗 / 启动 Agnet
// 「启动 Agnet」旁边写「参数由 Heicode 自动生成」。
// 本次会做 / 本次允许使用 / 本次不会做 / 高危规则 / 预计消耗 / 启动 Agent
// 「启动 Agent」旁边写「参数由 Heicode 自动生成」。
// 没有 JSON 编辑器、permission manifest、resource grant 表(§10 高级展开禁令)。
// =============================================================================
@@ -2859,12 +2859,12 @@ function RecommendationSummaryDialog({
const handleLaunch = () => {
setLaunching(true)
// Real /api/agnet/deployments POST is wired separately when the task
// Real /api/agent/deployments POST is wired separately when the task
// object backend lands. For now the summary card matches the docs spec
// visually; clicking captures intent + hands off to the desktop client.
setTimeout(() => {
toast.success(
t('Agnet launch staged. Continue the task in the desktop client.')
t('Agent launch staged. Continue the task in the desktop client.')
)
setLaunching(false)
onClose()
@@ -2880,7 +2880,7 @@ function RecommendationSummaryDialog({
{t('Recommendation summary')}
</p>
<h3 className='mt-1 text-lg font-semibold'>
{t('Confirm scope, risk and budget before launching Agnet')}
{t('Confirm scope, risk and budget before launching Agent')}
</h3>
<p className='text-muted-foreground mt-1 text-xs'>
{t(
@@ -2939,7 +2939,7 @@ function RecommendationSummaryDialog({
}}
>
<Rocket className='h-3.5 w-3.5' />
{launching ? t('Launching…') : t('Launch Agnet')}
{launching ? t('Launching…') : t('Launch Agent')}
</Button>
</div>
</footer>
@@ -2998,7 +2998,7 @@ function RecBlock({
// Templates / Agents (kept for backward compatibility — invoked by side routes)
// =============================================================================
export function AgnetTemplatesPage() {
export function AgentTemplatesPage() {
const { t } = useTranslation()
const templates = [
{
@@ -3035,7 +3035,7 @@ export function AgnetTemplatesPage() {
)
}
function runtimeSummary(rt: AgnetRuntimeExecution | undefined): boolean {
function runtimeSummary(rt: AgentRuntimeExecution | undefined): boolean {
if (!rt) return false
return Boolean(
(rt.profile_id && rt.profile_id.trim() !== '') ||
@@ -3044,7 +3044,7 @@ function runtimeSummary(rt: AgnetRuntimeExecution | undefined): boolean {
)
}
function policySummary(p: AgnetSKAccessPolicy | undefined): boolean {
function policySummary(p: AgentSKAccessPolicy | undefined): boolean {
if (!p) return false
return Boolean(
(p.policy_ref && p.policy_ref.trim() !== '') ||
@@ -3053,15 +3053,15 @@ function policySummary(p: AgnetSKAccessPolicy | undefined): boolean {
)
}
export function AgnetAgentsPage() {
export function AgentAgentsPage() {
const { t } = useTranslation()
const { data = [] } = useQuery({
queryKey: ['agnet', 'deployments'],
queryFn: listAgnetDeployments,
queryKey: ['agent', 'deployments'],
queryFn: listAgentDeployments,
})
const rows = useMemo(
() =>
data.flatMap((dep: AgnetDeployment) =>
data.flatMap((dep: AgentDeployment) =>
(dep.orchestration_plan?.agents || []).map((agent, idx) => ({
dep: dep.deployment_id,
id: `${dep.deployment_id}-${idx}`,
@@ -3078,7 +3078,7 @@ export function AgnetAgentsPage() {
return (
<PageSurface
title={t('Agents')}
subtitle={t('Agent declarations parsed from each Agnet deployment plan.')}
subtitle={t('Agent declarations parsed from each Agent deployment plan.')}
>
{rows.length === 0 ? (
<EmptySurface
@@ -3093,7 +3093,7 @@ export function AgnetAgentsPage() {
>
<p className='text-sm font-medium'>{row.role}</p>
<p className='text-muted-foreground mt-1 font-mono text-[11px] tracking-[0.12em] uppercase'>
{row.dep} · {t('Agnet runtime model')}: {row.runtimeModel}
{row.dep} · {t('Agent runtime model')}: {row.runtimeModel}
</p>
<p className='mt-2 text-sm'>{row.goal}</p>
{runtimeSummary(row.runtime) && (
@@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router'
type AgnetHubProps = {
type AgentHubProps = {
title: string
description: string
}
@@ -11,7 +11,7 @@ const quickLinks = [
{ title: 'Audit', to: '/audit' as const },
]
export function AgnetHub(props: AgnetHubProps) {
export function AgentHub(props: AgentHubProps) {
return (
<div className='mx-auto w-full max-w-5xl p-6'>
<div className='mb-5'>
+8 -8
View File
@@ -51,10 +51,10 @@ export function clearHeicodeTokens() {
}
/**
* 外部 Agnet 登录成功后,用 token 向本站校验身份并写入 Manager 会话 Cookie。
* 外部 Agent 登录成功后,用 token 向本站校验身份并写入 Manager 会话 Cookie。
* 不在本站再做密码校验;本地用户按需 JIT 创建。
*/
async function establishManagerSessionFromAgnet(): Promise<{
async function establishManagerSessionFromAgent(): Promise<{
managerUserId?: number
}> {
const access_token = readToken(ACCESS_TOKEN_KEY)
@@ -63,7 +63,7 @@ async function establishManagerSessionFromAgnet(): Promise<{
throw new Error('Missing Heicode access token')
}
const res = await api.post(
'/api/user/session/from-agnet',
'/api/user/session/from-agent',
{
access_token,
refresh_token: refresh_token || undefined,
@@ -101,13 +101,13 @@ async function establishManagerSessionFromAgnet(): Promise<{
return { managerUserId: body.data?.id }
}
export async function loginWithAgnetTokens(
export async function loginWithAgentTokens(
accessToken: string,
refreshToken?: string
): Promise<{ managerUserId?: number }> {
writeTokens(accessToken, refreshToken)
try {
return await establishManagerSessionFromAgnet()
return await establishManagerSessionFromAgent()
} catch (err) {
clearHeicodeTokens()
throw err
@@ -185,7 +185,7 @@ export async function login(payload: LoginPayload) {
if (res?.success) {
writeTokens(res.data?.token, res.data?.refreshToken)
try {
const sessionRes = await establishManagerSessionFromAgnet()
const sessionRes = await establishManagerSessionFromAgent()
managerUserId = sessionRes.managerUserId
} catch (syncErr) {
if (isTwoFactorRequiredError(syncErr)) {
@@ -330,7 +330,7 @@ export async function wechatLoginByCode(code: string): Promise<ApiResponse> {
// Registration
// ----------------------------------------------------------------------------
// User registration via Agnet (太极 AI PAD)
// User registration via Agent (太极 AI PAD)
export async function register(payload: RegisterPayload): Promise<ApiResponse & {
data?: {
token?: string
@@ -364,7 +364,7 @@ export async function register(payload: RegisterPayload): Promise<ApiResponse &
}
}
// Send email verification code via Agnet (太极 AI PAD)
// Send email verification code via Agent (太极 AI PAD)
export async function sendEmailVerification(
email: string,
_turnstile?: string
+1 -1
View File
@@ -58,7 +58,7 @@ export function AuthLayout({ children }: AuthLayoutProps) {
const pillars = [
{
icon: Workflow,
title: t('Agnet orchestration'),
title: t('Agent orchestration'),
desc: t('Plan, dispatch and monitor multi-agent runs across tenants.'),
},
{
@@ -77,7 +77,7 @@ export function useAuthRedirect() {
}
// 优先从本地 Manager 拉真实用户(含 role / status / quota),
// 因为 Agnet 上的 role 不一定与本地 JIT/管理员白名单同步后的角色一致。
// 因为 Agent 上的 role 不一定与本地 JIT/管理员白名单同步后的角色一致。
let userSet = false
try {
const selfRes = (await getSelf()) as {
@@ -100,7 +100,7 @@ export function useAuthRedirect() {
userSet = true
}
} catch {
// Fall through to Agnet /me / fallback below.
// Fall through to Agent /me / fallback below.
}
if (!userSet) {
@@ -28,7 +28,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile'
import { register, loginWithAgnetTokens, wechatLoginByCode } from '@/features/auth/api'
import { register, loginWithAgentTokens, wechatLoginByCode } from '@/features/auth/api'
import { LegalConsent } from '@/features/auth/components/legal-consent'
import { OAuthProviders } from '@/features/auth/components/oauth-providers'
import { registerFormSchema } from '@/features/auth/constants'
@@ -144,7 +144,7 @@ export function SignUpForm({
if (res?.success) {
toast.success(t('Account created!'))
if (res.data?.token) {
const sessionRes = await loginWithAgnetTokens(
const sessionRes = await loginWithAgentTokens(
res.data.token,
res.data.refreshToken
)
@@ -19,10 +19,10 @@ import {
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
getAgnetAuditLogs,
listAgnetDeployments,
type AgnetDeployment,
} from '@/features/agnet-console/api'
getAgentAuditLogs,
listAgentDeployments,
type AgentDeployment,
} from '@/features/agent-console/api'
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
@@ -145,18 +145,18 @@ export function CockpitView() {
const deploymentsQuery = useQuery({
queryKey: ['cockpit', 'deployments'],
queryFn: listAgnetDeployments,
queryFn: listAgentDeployments,
refetchInterval: 30_000,
})
const auditQuery = useQuery({
queryKey: ['cockpit', 'audit'],
queryFn: getAgnetAuditLogs,
queryFn: getAgentAuditLogs,
refetchInterval: 60_000,
})
const stats = useMemo(() => {
const list: AgnetDeployment[] = deploymentsQuery.data ?? []
const list: AgentDeployment[] = deploymentsQuery.data ?? []
const counters: Record<StatusKey, number> = {
running: 0,
success: 0,
@@ -189,7 +189,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
</h2>
<p className='text-muted-foreground mt-2 text-sm'>
{t(
'Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.'
'Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agent team.'
)}
</p>
</div>
@@ -293,7 +293,7 @@ function ContinueTasks({
) : recent.length === 0 ? (
<p className='bg-background/40 text-muted-foreground rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-4 text-center text-xs'>
{t(
'No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.'
'No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agent.'
)}
</p>
) : (
@@ -377,7 +377,7 @@ function TodayFocus({
tone: 'running',
title: t('Running'),
count: buckets.running.length,
hint: t('Active Agnet sub-loops'),
hint: t('Active Agent sub-loops'),
},
]
@@ -477,7 +477,7 @@ function HelperEntries({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
{
Icon: Rocket,
title: t('Task overview'),
desc: t('Status of every Agnet task you launched'),
desc: t('Status of every Agent task you launched'),
to: '/deployments',
},
]
@@ -15,7 +15,7 @@ const MODELS: ModelConfig[] = [
id: 'gpt-4o',
name: 'gpt-4o',
response:
'Agnet planners propose orchestration runs; the platform arbitrates risk and budget before any agent executes a step.',
'Agent planners propose orchestration runs; the platform arbitrates risk and budget before any agent executes a step.',
tokens: 27,
latency: 142,
badgeClass:
+6 -6
View File
@@ -45,9 +45,9 @@ import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import {
createAgnetDeployment,
type AgnetOrchestrationPlan,
} from '@/features/agnet-console/api'
createAgentDeployment,
type AgentOrchestrationPlan,
} from '@/features/agent-console/api'
const route = getRouteApi('/_authenticated/tasks/$id')
@@ -214,13 +214,13 @@ export function TaskCardView() {
role_templates: ['backend'],
default_model_id: 'claude-sonnet-4-6',
})
return createAgnetDeployment({
return createAgentDeployment({
orchestration_plan:
draft.orchestration_plan as unknown as AgnetOrchestrationPlan,
draft.orchestration_plan as unknown as AgentOrchestrationPlan,
})
},
onSuccess: (deployment) => {
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'deployments'] })
toast.success(
t('Manager deployment created', {
deployment_id: deployment.deployment_id,
+1 -1
View File
@@ -91,7 +91,7 @@ export function Wallet(props: WalletProps) {
// the canonical balance source is mcp-server /api/user/heicode/balance
// (server-side wraps NewAPI admin token, returns the freshest figures).
// Fall back to NewAPI /api/user/self when the user hasn't yet been
// mirrored into NewAPI via from-agnet (HEICODE_USER_NOT_FOUND) so a
// mirrored into NewAPI via from-agent (HEICODE_USER_NOT_FOUND) so a
// brand-new account still sees something instead of empty stats.
const fetchUser = useCallback(async () => {
try {
+3 -3
View File
@@ -10,9 +10,9 @@ export type TopNavLink = {
external?: boolean
}
// Default top-nav modules — agnet command axis only.
// Default top-nav modules — agent command axis only.
// Pricing/Models/Channels are intentionally absent: Heicode Manager
// is a tenant + agnet control plane, not an API gateway storefront.
// is a tenant + agent control plane, not an API gateway storefront.
const DEFAULT_HEADER_NAV_MODULES = {
home: true,
overview: true,
@@ -26,7 +26,7 @@ const DEFAULT_HEADER_NAV_MODULES = {
* Backend format example (stringified JSON):
* {
* home: true,
* agnet: true,
* agent: true,
* deployments: true,
* events: true,
* audit: true,
@@ -2,7 +2,7 @@
"Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.": "Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.",
"Adjust filters or trigger a new orchestration plan.": "Adjust filters or trigger a new orchestration plan.",
"Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.": "Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Click to view image": "Click to view image",
"Client login API contract and integration screenshots": "Client login API contract and integration screenshots",
"Create a new code delivery run with checks.": "Create a new code delivery run with checks.",
@@ -13,7 +13,7 @@
"Git sources subtitle": "Bind Git repositories for SK, then review immutable snapshot anchors tied to each deployment.",
"Git-backed SK sources description": "Immutable snapshots from bound Git refs and uploads—wired into each run for audit.",
"Immutable, hash-verified context bundles wired to every run.": "Immutable, hash-verified context bundles wired to every run.",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"No audit entries match the filter.": "No audit entries match the filter.",
"No resolved snapshots hint": "Resolve snapshots from the control plane after Git-backed sk_sources are configured.",
"No deployments match the current filter": "No deployments match the current filter",
@@ -24,7 +24,7 @@
"Reset filters to see all entries.": "Reset filters to see all entries.",
"Resolve snapshots from the control plane to capture SK lineage.": "Resolve snapshots from the control plane to capture SK lineage.",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.": "Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.",
@@ -13,7 +13,7 @@
"Agentic development control plane": "Agentic development control plane",
"Agents": "Agents",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AIGC2D": "AIGC2D",
"All statuses": "All statuses",
"All usage logs": "All usage logs",
@@ -26,7 +26,7 @@
"Awaiting platform arbitration": "Awaiting platform arbitration",
"AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *",
"budget": "budget",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"channel": "channel",
"checkout.session.completed": "checkout.session.completed",
"checkout.session.expired": "checkout.session.expired",
@@ -96,7 +96,7 @@
"Insufficient permission": "Insufficient permission",
"Jimeng": "Jimeng",
"JustSong": "JustSong",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"LingYiWanWu": "LingYiWanWu",
"LinuxDO": "LinuxDO",
"Live code delivery runs": "Live code delivery runs",
@@ -161,7 +161,7 @@
"Running deployments": "Running deployments",
"Select deployment": "Select deployment",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"SiliconFlow": "SiliconFlow",
@@ -16,7 +16,7 @@
"Agentic development control plane": "Agentic development control plane",
"Agents": "Agents",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AI Proxy": "AI Proxy",
"AIGC2D": "AIGC2D",
"All statuses": "All statuses",
@@ -30,7 +30,7 @@
"AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *",
"Baidu V2": "Baidu V2",
"budget": "budget",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"channel": "channel",
"checkout.session.completed": "checkout.session.completed",
"checkout.session.expired": "checkout.session.expired",
@@ -100,7 +100,7 @@
"Insufficient permission": "Insufficient permission",
"Jimeng": "Jimeng",
"JustSong": "JustSong",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"LingYiWanWu": "LingYiWanWu",
"LinuxDO": "LinuxDO",
"Live code delivery runs": "Live code delivery runs",
@@ -164,7 +164,7 @@
"Running deployments": "Running deployments",
"Select deployment": "Select deployment",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"SiliconFlow": "SiliconFlow",
@@ -2,7 +2,7 @@
"Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.": "Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.",
"Adjust filters or trigger a new orchestration plan.": "Adjust filters or trigger a new orchestration plan.",
"Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.": "Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Click to view image": "Click to view image",
"Client login API contract and integration screenshots": "Client login API contract and integration screenshots",
"Create a new code delivery run with checks.": "Create a new code delivery run with checks.",
@@ -13,7 +13,7 @@
"Git sources subtitle": "Bind Git repositories for SK, then review immutable snapshot anchors tied to each deployment.",
"Git-backed SK sources description": "Immutable snapshots from bound Git refs and uploads—wired into each run for audit.",
"Immutable, hash-verified context bundles wired to every run.": "Immutable, hash-verified context bundles wired to every run.",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"No audit entries match the filter.": "No audit entries match the filter.",
"No resolved snapshots hint": "Resolve snapshots from the control plane after Git-backed sk_sources are configured.",
"No deployments match the current filter": "No deployments match the current filter",
@@ -24,7 +24,7 @@
"Reset filters to see all entries.": "Reset filters to see all entries.",
"Resolve snapshots from the control plane to capture SK lineage.": "Resolve snapshots from the control plane to capture SK lineage.",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.": "Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.",
+30 -30
View File
@@ -78,7 +78,7 @@
"Actions": "Actions",
"active": "active",
"Active": "Active",
"Active Agnet sub-loops": "Active Agnet sub-loops",
"Active Agent sub-loops": "Active Agent sub-loops",
"Active Cache Count": "Active Cache Count",
"Active code delivery runs across tenants": "Active code delivery runs across tenants",
"Active Files": "Active Files",
@@ -176,7 +176,7 @@
"After invalidating, this subscription will be immediately deactivated. Historical records are not affected. Continue?": "After invalidating, this subscription will be immediately deactivated. Historical records are not affected. Continue?",
"After scanning, the binding will complete automatically": "After scanning, the binding will complete automatically",
"Agent": "Agent",
"Agent declarations parsed from each Agnet deployment plan.": "Agent declarations parsed from each Agnet deployment plan.",
"Agent declarations parsed from each Agent deployment plan.": "Agent declarations parsed from each Agent deployment plan.",
"Agent declarations parsed from each deployment plan.": "Agent declarations parsed from each deployment plan.",
"Agent ID *": "Agent ID *",
"Agentic development control plane": "Agentic development control plane",
@@ -184,12 +184,12 @@
"Aggregated usage metrics and trend charts.": "Aggregated usage metrics and trend charts.",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.",
"Agile Minimal": "Agile Minimal",
"Agnet allowed models comma": "Agnet runtime model IDs (comma-separated)",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agnet launch staged. Continue the task in the desktop client.": "Agnet launch staged. Continue the task in the desktop client.",
"Agnet orchestration": "Agnet orchestration",
"Agnet runtime model": "Agnet runtime model",
"Agnet runtime model id": "Agnet runtime model ID",
"Agent allowed models comma": "Agent runtime model IDs (comma-separated)",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Agent launch staged. Continue the task in the desktop client.": "Agent launch staged. Continue the task in the desktop client.",
"Agent orchestration": "Agent orchestration",
"Agent runtime model": "Agent runtime model",
"Agent runtime model id": "Agent runtime model ID",
"ago": "ago",
"AGPL v3.0 License": "AGPL v3.0 License",
"AI model testing environment": "AI model testing environment",
@@ -504,7 +504,7 @@
"Browse and compare": "Browse and compare",
"budget": "budget",
"Budget & usage": "Budget & usage",
"Budget caps": "Agnet runtime caps",
"Budget caps": "Agent runtime caps",
"Budget tokens = max tokens × ratio. Accepts a decimal between 0.002 and 1. Recommended to keep aligned with upstream billing.": "Budget tokens = max tokens × ratio. Accepts a decimal between 0.002 and 1. Recommended to keep aligned with upstream billing.",
"Budget tokens = max tokens × ratio. Accepts a decimal between 0.1 and 1.": "Budget tokens = max tokens × ratio. Accepts a decimal between 0.1 and 1.",
"Budget Tokens Ratio": "Budget Tokens Ratio",
@@ -547,7 +547,7 @@
"Cancelled": "Cancelled",
"Cancelled at": "Cancelled at",
"Capture a reusable bundle of models, tags, or endpoints.": "Capture a reusable bundle of models, tags, or endpoints.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, runtime caps, resource scope and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, runtime caps, resource scope and live status.",
"Category Name": "Category Name",
"Category name is required": "Category name is required",
"Category name must be less than 50 characters": "Category name must be less than 50 characters",
@@ -771,7 +771,7 @@
"Configure your account preferences and integrations": "Configure your account preferences and integrations",
"Confirm": "Confirm",
"Confirm Action": "Confirm Action",
"Confirm and launch Agnet": "Confirm and launch Agnet",
"Confirm and launch Agent": "Confirm and launch Agent",
"Confirm Batch Update": "Confirm Batch Update",
"Confirm Billing Conflicts": "Confirm Billing Conflicts",
"Confirm Changes": "Confirm Changes",
@@ -790,7 +790,7 @@
"Confirm password": "Confirm password",
"Confirm Payment": "Confirm Payment",
"Confirm recommendation summary": "Confirm recommendation summary",
"Confirm scope, risk and budget before launching Agnet": "Confirm scope, risk and budget before launching Agnet",
"Confirm scope, risk and budget before launching Agent": "Confirm scope, risk and budget before launching Agent",
"Confirm Selection": "Confirm Selection",
"Confirm settings and finish setup": "Confirm settings and finish setup",
"Confirm Unbind": "Confirm Unbind",
@@ -883,8 +883,8 @@
"Create a new code delivery run with checks.": "Create a new code delivery run with checks.",
"Create a new user group to configure ratio overrides for.": "Create a new user group to configure ratio overrides for.",
"Create account": "Create account",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the orchestration plan to Agnet. Model choices here are runtime policy, not billing setup.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the orchestration plan to Agent. Model choices here are runtime policy, not billing setup.",
"Create an account": "Create an account",
"Create and review invite or credit codes.": "Create and review invite or credit codes.",
"Create API Key": "Create API Key",
@@ -1040,7 +1040,7 @@
"Deployments": "Deployments",
"Desc": "Desc",
"Describe": "Describe",
"Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.": "Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.",
"Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agent team.": "Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agent team.",
"Describe this model...": "Describe this model...",
"Describe this vendor...": "Describe this vendor...",
"Describe what this work should achieve.": "Describe what this work should achieve.",
@@ -1181,7 +1181,7 @@
"e.g., us-central1 or JSON format for model-specific regions": "e.g., us-central1 or JSON format for model-specific regions",
"e.g., v2.1": "e.g., v2.1",
"Each backup code can only be used once.": "Each backup code can only be used once.",
"Each card maps one child Agnet role to a runtime model and one bounded resource grant.": "Each card maps one child Agnet role to a runtime model and one bounded resource grant.",
"Each card maps one child Agent role to a runtime model and one bounded resource grant.": "Each card maps one child Agent role to a runtime model and one bounded resource grant.",
"Each item must be an object with a single key-value pair.": "Each item must be an object with a single key-value pair.",
"Each item must have exactly one key-value pair.": "Each item must have exactly one key-value pair.",
"Each line represents one keyword. Leave blank to disable the list but keep the switch states.": "Each line represents one keyword. Leave blank to disable the list but keep the switch states.",
@@ -1677,8 +1677,8 @@
"Get Started": "Get Started",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) that Resource Grants allow this run to draw from.",
"Git sources workflow step 3": "Allocate cloud capacity and permissions for sub-agents—for example dedicated VMs, roles, and API scopes.",
@@ -1958,7 +1958,7 @@
"Last updated:": "Last updated:",
"Last Used": "Last Used",
"Last used:": "Last used:",
"Launch Agnet": "Launch Agnet",
"Launch Agent": "Launch Agent",
"Launching…": "Launching…",
"Layout": "Layout",
"Learn more": "Learn more",
@@ -1978,7 +1978,7 @@
"Leave empty to use system temp directory": "Leave empty to use system temp directory",
"Leave empty to use username": "Leave empty to use username",
"Less": "Less",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "Light",
"Lightning Fast": "Lightning Fast",
"Limit period": "Limit period",
@@ -2023,7 +2023,7 @@
"Logo": "Logo",
"Logo URL": "Logo URL",
"Logs": "Logs",
"Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.",
"Long-lived credentials are stored in the secret vault. Agent only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "Long-lived credentials are stored in the secret vault. Agent only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.",
"m": "m",
"Maintain a list of common questions for the dashboard help panel": "Maintain a list of common questions for the dashboard help panel",
"Maintenance": "Maintenance",
@@ -2381,7 +2381,7 @@
"No Sync": "No Sync",
"No system announcements": "No system announcements",
"No tasks yet": "No tasks yet",
"No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.": "No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.",
"No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agent.": "No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agent.",
"No token found.": "No token found.",
"No tools configured": "No tools configured",
"No Upgrade": "No Upgrade",
@@ -3039,7 +3039,7 @@
"Resources workflow step 1": "Bind project code, SK, document, or cloud-resource metadata without exposing raw credentials.",
"Resources workflow step 2": "Keep secret material behind secret_ref; Manager surfaces references and status only.",
"Resources workflow step 3": "Allocate scope, allowed paths/actions, runtime policy, and budget to the run manifest.",
"Resources workflow step 4": "Start a Work/Run; Agnet resolves immutable anchors and enforces the effective grants.",
"Resources workflow step 4": "Start a Work/Run; Agent resolves immutable anchors and enforces the effective grants.",
"Resources workflow step 5": "Use snapshots, events, and audit together to replay which resource context actually ran.",
"Resources workflow title": "Resource-to-run flow",
"Response": "Response",
@@ -3295,7 +3295,7 @@
"Sidebar Personal Settings": "Sidebar Personal Settings",
"Sign in": "Sign in",
"Sign In": "Sign In",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Sign in with Passkey",
@@ -3364,10 +3364,10 @@
"status": "status",
"Status & Sync": "Status & Sync",
"Status Code Mapping": "Status Code Mapping",
"Status of every Agnet task you launched": "Status of every Agnet task you launched",
"Status of every Agent task you launched": "Status of every Agent task you launched",
"Status Page Slug": "Status Page Slug",
"Status, errors and budget burn in one auditable stream.": "Status, errors and budget burn in one auditable stream.",
"Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.": "Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.",
"Status, latest activity and last update for every Agent task you launched. Details live in the desktop client.": "Status, latest activity and last update for every Agent task you launched. Details live in the desktop client.",
"Status:": "Status:",
"Stay": "Stay",
"Stay tuned though!": "Stay tuned though!",
@@ -3480,7 +3480,7 @@
"Task Logs": "Task Logs",
"Task not found. It may have been removed or was never created.": "Task not found. It may have been removed or was never created.",
"Task overview": "Task overview",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agent.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agent.",
"Runtime diagnostics": "Runtime diagnostics",
"Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.": "Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.",
"runtime mode": "runtime mode",
@@ -3685,7 +3685,7 @@
"Total:": "Total:",
"TPM": "TPM",
"Trace delivery context snapshots by hash.": "Trace delivery context snapshots by hash.",
"Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.": "Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.",
"Track every Agent work run by status, risk, budget, scope and secret_ref coverage.": "Track every Agent work run by status, risk, budget, scope and secret_ref coverage.",
"Track per-request consumption to power usage analytics. Keeping this on increases database writes.": "Track per-request consumption to power usage analytics. Keeping this on increases database writes.",
"Track usage, costs and performance with real-time analytics": "Track usage, costs and performance with real-time analytics",
"Tracks current account base limits and additional metered usage on Codex upstream.": "Tracks current account base limits and additional metered usage on Codex upstream.",
@@ -4112,7 +4112,7 @@
"This run will do": "This run will do",
"No objective provided yet — go back to the Idea tab.": "No objective provided yet — go back to the Idea tab.",
"Resources this run may use": "Resources this run may use",
"No resources bound yet — Agnet will run with no external data access.": "No resources bound yet — Agnet will run with no external data access.",
"No resources bound yet — Agent will run with no external data access.": "No resources bound yet — Agent will run with no external data access.",
"no actions specified": "no actions specified",
"This run will NOT do": "This run will NOT do",
"Production deploys without client approval": "Production deploys without client approval",
@@ -4143,7 +4143,7 @@
"No audit events yet for this deployment.": "No audit events yet for this deployment.",
"Connect project docs": "Connect project docs",
"{{n}} doc source connected": "{{n}} doc source connected",
"Link product requirements, design docs or wiki repos so Agnet has project context.": "Link product requirements, design docs or wiki repos so Agnet has project context.",
"Link product requirements, design docs or wiki repos so Agent has project context.": "Link product requirements, design docs or wiki repos so Agent has project context.",
"Connect SK skill packs": "Connect SK skill packs",
"{{n}} SK source connected": "{{n}} SK source connected",
"Pick a reusable skill / agent toolset repository, or skip.": "Pick a reusable skill / agent toolset repository, or skip.",
+9 -9
View File
@@ -211,12 +211,12 @@
"Inherits deployment defaults": "Inherits deployment defaults",
"Add agent": "Add agent",
"Agent": "Agent",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Allowed models comma": "Allowed model IDs (comma-separated)",
"Budget caps": "Budget caps",
"Cloud principals comma": "Cloud principals (comma-separated)",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agnet.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agent.",
"Default model id": "Default model ID",
"Deployment plan": "Deployment plan",
"Deployment request failed": "Deployment request failed",
@@ -236,7 +236,7 @@
"Aggregated usage metrics and trend charts.": "Métriques d'utilisation agrégées et graphiques de tendances.",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "agrège plus de 50 fournisseurs IA derrière une API unifiée. Gérez l'accès, suivez les coûts et évoluez sans effort.",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AGPL v3.0 License": "Licence AGPL v3.0",
"AI model testing environment": "Environnement de test de modèle IA",
"AI models": "Modèles d'IA",
@@ -566,7 +566,7 @@
"Cancelled": "Annulé",
"Cancelled at": "Annulé le",
"Capture a reusable bundle of models, tags, or endpoints.": "Capturez un ensemble réutilisable de modèles, d'étiquettes ou de points de terminaison.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Category Name": "Nom de la catégorie",
"Category name is required": "Le nom de la catégorie est requis",
"Category name must be less than 50 characters": "Le nom de la catégorie doit contenir moins de 50 caractères",
@@ -1640,8 +1640,8 @@
"Get Started": "Commencer",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow title": "Typical setup flow",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) your tenant is allowed to draw from.",
@@ -1918,7 +1918,7 @@
"Leave empty to use system temp directory": "Laisser vide pour utiliser le répertoire temporaire",
"Leave empty to use username": "Laissez vide pour utiliser le nom d'utilisateur",
"Less": "Moins",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "Clair",
"Lightning Fast": "Extrêmement rapide",
"Limit period": "Période de limite",
@@ -3146,7 +3146,7 @@
"Sidebar Personal Settings": "Paramètres personnels de la barre latérale",
"Sign in": "Se connecter",
"Sign In": "Se connecter",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Se connecter avec Passkey",
+9 -9
View File
@@ -211,12 +211,12 @@
"Inherits deployment defaults": "Inherits deployment defaults",
"Add agent": "Add agent",
"Agent": "Agent",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Allowed models comma": "Allowed model IDs (comma-separated)",
"Budget caps": "Budget caps",
"Cloud principals comma": "Cloud principals (comma-separated)",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agnet.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agent.",
"Default model id": "Default model ID",
"Deployment plan": "Deployment plan",
"Deployment request failed": "Deployment request failed",
@@ -236,7 +236,7 @@
"Aggregated usage metrics and trend charts.": "集計された使用量メトリクスとトレンドチャート。",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "50以上のAIプロバイダーを統一APIで集約。アクセス管理、コスト追跡、スケーリングを簡単に。",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AGPL v3.0 License": "AGPL v3.0ライセンス",
"AI model testing environment": "AIモデルテスト環境",
"AI models": "AIモデル",
@@ -566,7 +566,7 @@
"Cancelled": "キャンセル",
"Cancelled at": "キャンセル日時",
"Capture a reusable bundle of models, tags, or endpoints.": "モデル、タグ、またはエンドポイントの再利用可能なバンドルを保存。",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Category Name": "分類名称",
"Category name is required": "カテゴリ名は必須です",
"Category name must be less than 50 characters": "カテゴリ名は50文字以内にしてください",
@@ -1640,8 +1640,8 @@
"Get Started": "開始する",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow title": "Typical setup flow",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) your tenant is allowed to draw from.",
@@ -1918,7 +1918,7 @@
"Leave empty to use system temp directory": "空欄でシステムの一時ディレクトリを使用",
"Leave empty to use username": "ユーザー名を使用するには空のままにしてください",
"Less": "少ない",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "ライト",
"Lightning Fast": "超高速",
"Limit period": "制限期間",
@@ -3146,7 +3146,7 @@
"Sidebar Personal Settings": "サイドバー個人設定",
"Sign in": "ログイン",
"Sign In": "ログイン",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Passkeyでログイン",
+9 -9
View File
@@ -211,12 +211,12 @@
"Inherits deployment defaults": "Inherits deployment defaults",
"Add agent": "Add agent",
"Agent": "Agent",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Allowed models comma": "Allowed model IDs (comma-separated)",
"Budget caps": "Budget caps",
"Cloud principals comma": "Cloud principals (comma-separated)",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agnet.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agent.",
"Default model id": "Default model ID",
"Deployment plan": "Deployment plan",
"Deployment request failed": "Deployment request failed",
@@ -236,7 +236,7 @@
"Aggregated usage metrics and trend charts.": "Агрегированные метрики использования и графики трендов.",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "объединяет 50+ ИИ-провайдеров за единым API. Управляйте доступом, отслеживайте затраты и масштабируйтесь без усилий.",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AGPL v3.0 License": "Лицензия AGPL v3.0",
"AI model testing environment": "Среда тестирования ИИ моделей",
"AI models": "Модели ИИ",
@@ -566,7 +566,7 @@
"Cancelled": "Отменено",
"Cancelled at": "Отменено",
"Capture a reusable bundle of models, tags, or endpoints.": "Создайте повторно используемый набор моделей, тегов или конечных точек.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Category Name": "Название категории",
"Category name is required": "Название категории обязательно",
"Category name must be less than 50 characters": "Название категории должно содержать менее 50 символов",
@@ -1640,8 +1640,8 @@
"Get Started": "Начать",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow title": "Typical setup flow",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) your tenant is allowed to draw from.",
@@ -1918,7 +1918,7 @@
"Leave empty to use system temp directory": "Оставьте пустым для системного временного каталога",
"Leave empty to use username": "Оставьте пустым, чтобы использовать имя пользователя",
"Less": "Меньше",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "Светлая",
"Lightning Fast": "Молниеносно быстро",
"Limit period": "Период ограничения",
@@ -3146,7 +3146,7 @@
"Sidebar Personal Settings": "Личные настройки боковой панели",
"Sign in": "Войти",
"Sign In": "Войти",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Войти с Passkey",

Some files were not shown because too many files have changed in this diff Show More