feat: add azure key vault resource discovery

This commit is contained in:
gongzhiyong
2026-05-25 17:41:43 +08:00
parent 23ec354eb5
commit e1b9de2a49
48 changed files with 1649 additions and 694 deletions
+6
View File
@@ -3,6 +3,12 @@
# Build outputs
**/dist/
!heicode/web/default/dist/
heicode/web/default/dist/*
!heicode/web/default/dist/index.html
!heicode/web/classic/dist/
heicode/web/classic/dist/*
!heicode/web/classic/dist/index.html
**/.next/
**/out/
**/.rsbuild/
+1 -1
View File
@@ -143,5 +143,5 @@ Heicode 今日工作总结与后续计划
六、安全说明
- 本次文档和代码未写入任何真实密码、Token、Redis 密钥、PostgreSQL 密码、SSH 密码或 Git 密码。
- 文档示例全部使用占位符或 vault:// secret_ref。
- 文档示例使用占位符或 Azure Key Vault `azkv://...` secret_ref。
- 后续部署与推送仍需继续遵守:密钥只走环境变量、Secret Store、交互式凭据或平台安全注入,不进入 Git。
+1 -1
View File
@@ -6,7 +6,7 @@
|------|------|
| [`heicode.md`](./heicode.md) | Heicode 当前产品定位、系统边界和架构共识 |
| [`plan.md`](./plan.md) | 按当前共识拆出的实施计划 |
| [`heicode-runtime-auth-newapi-secret-design.md`](./heicode-runtime-auth-newapi-secret-design.md) | 用户输入、登录用户复用、NewAPI 扣费映射、OpenBao 短期凭证注入边界 |
| [`heicode-runtime-auth-newapi-secret-design.md`](./heicode-runtime-auth-newapi-secret-design.md) | 用户输入、登录用户复用、NewAPI 扣费映射、Azure Key Vault 凭证托管与短期凭证注入边界 |
| [`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 生产部署前的安全守卫、环境变量注入和验证计划 |
@@ -2,7 +2,7 @@
本文用于 Heicode Manager / Agnet / NewAPI 相关生产发布前的人工执行检查。它只描述安全命令、环境变量名和验证项,不保存任何真实地址、账号、密码、Token、连接串、SSH key 或云访问密钥。
适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、OpenBao 内网密钥保管、Agnet 平台联调、NewAPI 网关能力验证。
适用范围:Azure VM、Azure PostgreSQL、Azure Redis、Git 同步、Nginx 统一入口、Heicode Manager 容器、Azure Key Vault 密钥保管、Agnet 平台联调、NewAPI 网关能力验证。
## 1. 执行原则
@@ -12,7 +12,7 @@
| Git 发布 | 仅允许快进同步已审核提交;禁止在生产 VM 上提交代码或保存临时补丁。 |
| 数据库/Redis | Azure PostgreSQL / Redis 连接串只写入 VM 本地 `.env` 或 Secret Store;验证时只打印变量名和连通性结果,不打印值。 |
| Agnet / NewAPI | Manager 只传 `secret_ref`、部署计划、资源授权和审计上下文;禁止把明文云账号、数据库密码、模型 Key 放入请求体。 |
| OpenBao | 只允许 Manager/Agnet 受控网络访问;如果 Manager 提供客户端验证和绑定接口,OpenBao 不暴露公网路由。 |
| Azure Key Vault | 只允许 Manager/Agnet 通过 Managed Identity 和受控网络访问;如果 Manager 提供客户端验证和绑定接口,Key Vault 不暴露普通公网访问。 |
| 生产动作 | 执行 `up -d`、迁移、重启、回滚前必须记录当前镜像/提交和健康检查 URL;失败时停止扩大变更。 |
## 2. 本地发布前检查
@@ -69,6 +69,7 @@ REDIS_CONN_STRING=<azure-redis-connection-string>
SESSION_SECRET=<generated-session-secret>
HEICODE_ROOT_EMAILS=<comma-separated-root-emails-if-needed>
HEICODE_ADMIN_EMAILS=<comma-separated-admin-emails-if-needed>
AZURE_KEY_VAULT_URL=<azure-key-vault-url>
```
验证只打印 key,不打印 value:
@@ -96,8 +97,8 @@ ssh "$REMOTE" "cd '$REMOTE_DIR/heicode' && docker compose -f docker-compose.azur
- 对外域名统一使用 `heicode.xinghanlab.com`。
- Nginx 负责按路由转发 Manager 与 NewAPI,例如 Manager 主站、NewAPI 受控 API 或健康检查路由。
- OpenBao 仅供 Manager/Agnet 服务端访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。
- 若需要 OpenBao 运维 UI,也必须走临时 SSH tunnel、VPN、内网跳板或单独受保护管理入口,不走普通 SaaS 用户路由。
- Azure Key Vault 仅供 Manager/Agnet 服务端通过 Managed Identity 访问,不通过 `heicode.xinghanlab.com` 暴露给浏览器用户。
- Key Vault 应优先使用 Private Endpoint;如临时启用公网访问,必须限制来源并在验证后关闭。
构建和启动:
@@ -156,7 +157,7 @@ curl -fsS -X POST "$AGNET_BASE_URL/api/agnet/deployments" \
|---|---|---|
| 服务健康 | `curl -fsS http://127.0.0.1:3000/api/status` | 返回 `success=true`。 |
| Nginx 公网入口 | `curl -fsS https://heicode.xinghanlab.com/api/status` | 返回 Manager 健康状态;DNS 需解析到生产入口。 |
| OpenBao 暴露面 | `curl -fsSI https://heicode.xinghanlab.com/v1/sys/health` | 普通公网入口不应返回 OpenBao 健康信息;预期为无路由、403 或 404。 |
| Key Vault 暴露面 | Azure Portal/CLI 检查 Key Vault networking | 生产优先 `Public network access = Disabled`,通过 Private Endpoint 访问。 |
| 容器状态 | `docker compose ... ps heicode` | `heicode` 为 running/healthy。 |
| Git 版本 | `git rev-parse --short HEAD` | 与已审核提交一致。 |
| DB/Redis 注入 | `awk -F= ... .env` | 只打印 key,包含 `SQL_DSN`、`REDIS_CONN_STRING`。 |
@@ -2,7 +2,7 @@
日期:2026-05-04
本文修正 Manager、Agnet 平台、NewAPI 与 OpenBao 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。
本文修正 Manager、Agnet 平台、NewAPI 与 Azure Key Vault 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。
## 一、用户输入在哪里
@@ -60,7 +60,7 @@ JWT 中也包含:
Manager 可以有本地 user cache,但 canonical user identity 应来自登录接口返回的用户信息。除非未来产品明确引入企业组织、空间或项目账本,否则不要把 tenant/project 作为认证和扣费主轴。
## 三、高危操作审批与 OpenBao 密钥注入
## 三、高危操作审批与 Azure Key Vault 密钥注入
高危操作审批只在客户端完成。用户在客户端明确批准后,Manager/Agnet 平台才可以执行对应动作。Agnet 平台不是审批主体,不发起额外审批;它只校验 `approval_id`、审批主体、审批范围、TTL、`risk_level` 和策略是否匹配。
@@ -69,13 +69,13 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
```text
长期密钥
-> 用户授权或绑定资源
-> Manager 写入 OpenBao
-> Manager 通过 VM Managed Identity 写入 Azure Key Vault
-> Manager DB 只保存 secret_ref
-> OpenBao 只允许 Manager/Agnet 受控网络访问,不对公网暴露
-> Azure Key Vault 通过 Private Endpoint / 防火墙限制,只允许 Manager/Agnet 受控网络访问
高危操作
-> 客户端审批
-> Manager/Agnet 平台按 secret_ref 从 OpenBao 获取或派生短期凭证
-> Manager/Agnet 平台按 secret_ref 从 Azure Key Vault 获取或派生短期凭证
-> 短期凭证可注入子 Agnet
-> 子 Agnet 完成任务后凭证过期或撤销
```
@@ -86,15 +86,15 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
- `heicode.xinghanlab.com` 是 Manager、NewAPI 与内部服务的统一公网域名入口。
- Nginx 可以为 Manager 和 NewAPI 制定路由,例如 Manager 主站与 NewAPI 受控 API 路由。
- OpenBao 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 OpenBao。
- OpenBao 访问应限制在容器网络、VM loopback、AKS 内网、Workload Identity 或其它受保护服务间通道。
- Azure Key Vault 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 Azure Key Vault。
- Azure Key Vault 访问应限制在 Private Endpoint、Azure 防火墙规则、VM Managed Identity、AKS Workload Identity 或其它受保护服务间通道。
OpenBao 不暴露公网的检查口径:
Azure Key Vault 不暴露普通公网入口的检查口径:
- 公网 DNS、Nginx `server_name`、Ingress、LoadBalancer 和安全组规则不得直接指向 OpenBao 服务端口。
- Key Vault 应优先关闭 Public Network Access,并通过 Private Endpoint 接入 `heicode-vnet`。
- 外部客户端只能通过 Manager 的认证、资源绑定、审批和 `secret_ref` 管理接口间接操作密钥引用。
- Manager、Agnet 平台和子 Agnet 访问 OpenBao 时必须走内网地址、loopback、容器网络、AKS private endpoint 或 Workload Identity 绑定。
- 健康检查和联调报告只能证明内网访问可用;不得把公网可访问的 OpenBao health endpoint 作为验收口径。
- Manager、Agnet 平台和子 Agnet 访问 Azure Key Vault 时必须走 Private Endpoint / Workload Identity / Managed Identity 绑定。
- 健康检查和联调报告只能证明受控网络访问可用;不得把公网可访问作为验收口径。
短期凭证注入必须满足:
@@ -183,7 +183,7 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上
]
},
"secret_context": {
"secret_refs": ["vault://secret/resources/res_git_1"],
"secret_refs": ["azkv://heicode-kv.vault.azure.net/secrets/res_git_1"],
"inject_short_lived_credentials": true,
"approval_id": "approval_123"
}
@@ -192,7 +192,7 @@ Manager 请求 Agnet 平台部署或执行任务时,应携带登录用户上
这里的 `user_context` 用于确认身份,`billing_context` 用于 NewAPI 余额、用量和扣费映射,`agent_runtime` 用于 Agnet 平台独立选择子 Agnet 模型和实例数量,`work_context` 用于团队开发控制,`secret_context` 只传 `secret_ref` 和审批结果。
Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 OpenBao 凭证由平台安全通道读取,不进入 Markdown。
Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet model profile 和 NewAPI 映射写入 metadata 或审计日志。真实 NewAPI key 和 Azure Key Vault 凭证由平台安全通道读取,不进入 Markdown。
## 六、当前主线结论
@@ -202,5 +202,5 @@ Agnet 平台执行时,应把用户、角色、Git 绑定、deployment、Agnet
- 团队开发控制优先由绑定 Git、允许路径、Agnet 角色、资源授权表达。
- 子 Agnet 的运行模型由 Agnet 平台独立配置,不和 NewAPI 扣费对象混在一起。
- NewAPI 负责模型网关、用户/Token/Group 额度、余额、日志和扣费。
- OpenBao 负责长期密钥托管,子 Agnet 只拿短期、最小权限、可审计凭证。
- Azure Key Vault 负责长期密钥托管,子 Agnet 只拿短期、最小权限、可审计凭证。
- 高危操作审批发生在客户端,审批结果随任务上下文传给 Manager/Agnet 平台。
+4 -4
View File
@@ -39,7 +39,7 @@ Manager 是用户操作入口;Agnet 是运行时执行层;NewAPI 是模型
7. Manager 负责资源绑定、权限分配和凭证托管能力;真实密钥放入 Secret Store。
8. 子 Agnet 不保存长期密钥,只接收角色、资源元数据、AGENT.md 和受控访问方式。
9. Agnet 平台在 AKS 上负责运行时身份、隔离、状态、事件和审计回传。
10. 高危操作审批只在客户端完成;审批通过后可以把 OpenBao 派生的短期、最小权限凭证注入子 Agnet,但不能注入长期密钥。
10. 高危操作审批只在客户端完成;审批通过后可以把密钥保管器派生的短期、最小权限凭证注入子 Agnet,但不能注入长期密钥。
11. 子 Agnet 的运行模型是 Agnet 平台的部署配置,独立于 NewAPI 的用户、Token、Group 扣费映射。
## 四、Manager 的核心功能
@@ -130,7 +130,7 @@ P1 permission manifest 示例:
"ref": "main",
"paths": ["services/api/**"]
},
"secret_ref": "vault://secret/resources/repo_demo"
"secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/repo-demo"
}
]
}
@@ -154,7 +154,7 @@ Secret Broker 负责:
- 接收 OAuth、GitHub App、云授权回调后的凭证。
- 生成按用户、资源和角色隔离的 secret path。
- 写入 Vault、Infisical 或 Azure Key Vault。
- 写入 Azure Key Vault。
- 创建或更新 policy。
- 保存 `secret_ref` 到 Manager DB。
- 轮换、撤销、禁用凭证。
@@ -185,7 +185,7 @@ Agnet 平台底层是 AKS,因此运行时权限应和 Kubernetes 身份绑定
| 受控注入 | Git clone、开发/测试环境、低风险资源 |
| 短期凭证注入 | 已经客户端审批的生产部署、数据库写入、高危云操作 |
普通开发资源可受控注入;生产云资源和高危操作也只在客户端审批通过后,注入 OpenBao 派生的短期、最小权限凭证。
普通开发资源可受控注入;生产云资源和高危操作也只在客户端审批通过后,注入密钥保管器派生的短期、最小权限凭证。
## 八、NewAPI 边界
@@ -48,7 +48,7 @@ AGNET_PLATFORM_BASE_URL=https://agnet-platform.example.com
```text
AGNET_PLATFORM_BASE_URL=https://staging-agnet.example.com
MANAGER_SERVICE_TOKEN_SECRET_REF=vault://secret/users/manager-service/bindings/agnet-platform/service-token
MANAGER_SERVICE_TOKEN_SECRET_REF=azkv://heicode-kv.vault.azure.net/secrets/manager-service-agnet-platform-service-token
```
完整路径示例:
@@ -230,7 +230,7 @@ POST /api/agnet/deployments
"repo_url": "https://example.com/org/repo.git"
},
"status": "active",
"secret_ref": "vault://secret/users/user_123/bindings/repo_default/resources/res_git_repo_001",
"secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-repo-default-res-git-repo-001",
"audit": {
"created_by": "manager",
"approval_id": "approval_001"
@@ -419,7 +419,7 @@ POST /api/agnet/deployments
"service_name": "new-api"
},
"status": "active",
"secret_ref": "vault://secret/users/user_123/bindings/newapi-prod/resources/res_newapi_vm",
"secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-newapi-prod-res-newapi-vm",
"audit": {
"created_by": "manager",
"approval_id": "approval_newapi_001"
@@ -441,11 +441,11 @@ POST /api/agnet/deployments
"plaintext_export_forbidden": "true"
},
"metadata": {
"secret_provider": "openbao",
"secret_provider": "azure_key_vault",
"scope": "newapi-runtime"
},
"status": "active",
"secret_ref": "vault://secret/users/user_123/bindings/newapi-prod/resources/res_newapi_runtime_env",
"secret_ref": "azkv://heicode-kv.vault.azure.net/secrets/user-123-newapi-prod-res-newapi-runtime-env",
"audit": {
"created_by": "manager",
"approval_id": "approval_newapi_001"
@@ -912,7 +912,7 @@ Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Ag
| 审批范围 | 审批范围必须覆盖 `binding_scope`、`permission_scope`、目标环境、资源 ID 和操作类型。 |
| TTL / 时间窗口 | 审批记录必须未过期;若使用 `window` 或 TTL,当前执行时间必须落在允许范围内。 |
| `risk_level` | 高危资源写入、生产部署、云资源修改、删除和扩容必须为 `high`。 |
| 策略 | 平台 policy、OpenBao policy、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 |
| 策略 | 平台 policy、Key Vault 访问策略、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 |
任一校验不通过时,Agnet 平台应返回 `POLICY_REJECTED` 或 `FORBIDDEN_SCOPE`,不得发起额外批准流程。
+3 -3
View File
@@ -57,7 +57,7 @@
任务:
- 优先选型 HashiCorp Vault。
- 保留 Infisical 和 Azure Key Vault 作为 Secret Provider 备选。
- 当前生产方向使用 Azure Key Vault 作为 Secret Provider。
- 在 Manager 后端实现 Secret Broker。
- Secret Broker 负责写入、轮换、撤销、禁用和审计。
- Manager DB 只保存 `secret_ref`,不保存明文密钥。
@@ -76,10 +76,10 @@
- Agnet 平台支持 deployment / role 到 Kubernetes ServiceAccount 的映射。
- 支持 Vault Kubernetes Auth 或等价 Workload Identity。
- 支持按 user / resource binding / role 生成 OpenBao policy。
- 支持按 user / resource binding / role 生成密钥访问策略。
- 子 Agnet 运行时只能访问被授权的 secret。
- 普通开发资源支持受控注入。
- 高危操作审批只在客户端完成;审批通过后允许向子 Agnet 注入 OpenBao 派生的短期、最小权限凭证。
- 高危操作审批只在客户端完成;审批通过后允许向子 Agnet 注入密钥保管器派生的短期、最小权限凭证。
验收:
+1 -1
View File
@@ -38,7 +38,7 @@ Heicode 是一款覆盖软件生命周期的智能开发 Code 工具。它面向
| Heicode Manager / 浏览器控制台 | 辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态总览、模型余额与用量、审计与日志查看 |
| Agnet 平台 | 执行层,负责部署和运行子 Agnet,在任务推进过程中完成需求、开发、测试、修复、交付与部署,并回传日志、状态、事件和指标 |
| CodeGW | 内部模型网关与计费服务,普通用户不直接进入后台 |
| 密钥保管器 | OpenBao 实现,保存长期凭证,按审批和权限提供短期凭证租约 |
| 密钥保管器 | Azure Key Vault 实现,保存长期凭证引用,按审批和权限提供短期访问能力 |
说明:Heicode 客户端是用户主体验,用户不在网页上编码。Heicode Manager 是浏览器里的辅助控制台,承担资源、部署、状态、余额、审计和下载等辅助操作。真正持续推进任务的是 Heicode 调度下的 Agnet 执行闭环,Agnet 在过程中还可以调用已授权的 SK 工具和外部能力,最终完成交付与部署并把结果回传到客户端。
@@ -136,7 +136,7 @@
| 客户端 | 主对话体验、任务推进和高危审批 |
| Agnet 平台 | AKS 执行层 |
| CodeGW | 模型网关与用量 |
| OpenBao | 密钥保管器 |
| Azure Key Vault | 密钥保管器 |
补充说明:
@@ -170,4 +170,4 @@
- 信息密度适中,避免空泛大词。
- 主色与 Heicode 和客户端保持一致。
- 图片优先使用真实界面和流程,不使用无意义装饰图。
- 首页文案避免出现 CodeGW 后台、OpenBao 控制台、模型提供方配置等内部概念。
- 首页文案避免出现 CodeGW 后台、Azure Key Vault 控制台、模型提供方配置等内部概念。
@@ -12,7 +12,7 @@
https://code.xinghanlab.com
```
用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 OpenBao。
用户只需要登录 Heicode。普通用户不需要登录 CodeGW 后台,也不需要登录 Azure Key Vault。
## 第一步:登录 Heicode
+5 -5
View File
@@ -65,7 +65,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
讲述:
用户不需要直接操作 CodeGW 后台,也不需要直接管理 OpenBao。所有操作都在 Heicode 和客户端内完成。
用户不需要直接操作 CodeGW 后台,也不需要直接管理 Azure Key Vault。所有操作都在 Heicode 和客户端内完成。
## Slide 5:核心能力
@@ -91,13 +91,13 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
-> Heicode Manager
-> Agnet 平台
-> CodeGW
-> OpenBao
-> Azure Key Vault
-> Git / SK / 云资源
```
讲述:
客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。OpenBao 是密钥保管器。
客户端是主体验,Manager 是辅助控制台。Agnet 平台是执行层,执行过程中可调用 SK。CodeGW 是模型网关和计费服务。Azure Key Vault 是密钥保管器。
## Slide 7:安全模型
@@ -108,7 +108,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
要点:
- Heicode 服务端数据库只保存 `secret_ref`。
- 长期密钥进入 OpenBao。
- 长期密钥进入 Azure Key Vault。
- 子 Agnet 只获得短期、最小权限凭证。
- 高危操作必须在客户端审批。
- 日志、Markdown、前端响应不包含明文密钥。
@@ -141,7 +141,7 @@ Heicode 是一款全流程智能开发 Code 工具。它让用户输入想法,
> 当前 MVP 先闭环三件事
1. Heicode、CodeGW、OpenBao 解耦部署。
1. Heicode、CodeGW、Azure Key Vault 解耦部署。
2. 资源绑定、Resource Grant、manifest 和密钥保管。
3. 客户端只登录 Heicode,只使用 Heicode 模型。
@@ -26,17 +26,17 @@ Heicode 是 SaaS 产品,不能把凭证管理转嫁给用户,也不能让密
用户可见名称:密钥保管器。
技术实现:OpenBao,兼容 Vault API。
技术实现:Azure Key Vault。生产环境中 Manager 通过 VM Managed Identity 访问 Key Vault,不在业务数据库保存明文密钥。
用户不需要直接登录 OpenBao,也不需要看到 OpenBao 控制台。
用户不需要直接登录 Azure Key Vault,也不需要看到 Key Vault 控制台。
## Secret Broker 流程
```text
用户绑定资源
-> Heicode 接收授权结果
-> Heicode Secret Broker 写入 OpenBao
-> OpenBao 返回或形成 secret_ref
-> Heicode Secret Broker 写入 Azure Key Vault
-> Azure Key Vault 返回或形成 azkv://... 形式的 secret_ref
-> Heicode DB 保存 secret_ref
-> 前端只展示脱敏引用和状态
```
@@ -87,7 +87,7 @@ Heicode 生成 Resource Grant
3. 禁止前端返回明文密钥。
4. 禁止在日志中打印密钥。
5. 禁止把长期云密钥注入子 Agnet。
6. 禁止把 OpenBao 暴露为普通公网入口。
6. 禁止把 Azure Key Vault 管理入口作为普通用户入口暴露。
7. 禁止把 CodeGW key 原文交给客户端长期保存。
## 用户侧解释
@@ -105,4 +105,4 @@ Heicode 生成 Resource Grant
| 日志 | 不包含 token、password、private key、access key |
| 子 Agnet | 不保存长期凭证 |
| 高危操作 | 有客户端审批记录 |
| OpenBao | 不对普通公网暴露 |
| Azure Key Vault | 普通用户不可直接进入;Manager 仅通过受控后端身份访问 |
@@ -1,8 +1,8 @@
# 07. CodeGW、OpenBao、Agnet 平台边界说明
# 07. CodeGW、Azure Key Vault、Agnet 平台边界说明
## 目标
本文用于讲清楚 Heicode 与 CodeGW、OpenBao、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。
本文用于讲清楚 Heicode 与 CodeGW、Azure Key Vault、Agnet 平台之间的产品和技术边界,避免再次变成缝合式系统。
## 总体边界
@@ -10,7 +10,7 @@
|------|----------|----------------------|
| Heicode | 产品总入口,包含客户端主体验与 Manager 辅助控制台 | 是 |
| CodeGW | 模型网关、模型调用、余额、额度、用量日志 | 否 |
| OpenBao | 长期凭证保管、短期凭证租约 | 否 |
| Azure Key Vault | 长期凭证托管、密钥引用、后端受控读取 | 否 |
| Agnet 平台 | AKS 执行层、状态、日志、事件、指标 | 否 |
## Heicode 与 CodeGW
@@ -34,20 +34,20 @@ Heicode 不做:
> CodeGW 是 Heicode 的模型网关和计费底座,用户在 Heicode 中查看模型和用量,不直接使用 CodeGW 后台。
## Heicode 与 OpenBao
## Heicode 与 Azure Key Vault
Heicode 使用 OpenBao 的方式:
Heicode 使用 Azure Key Vault 的方式:
- 写入用户授权后的长期凭证。
- 保存和管理 `secret_ref`。
- 执行密钥 health、metadata、轮换、禁用、撤销。
- 执行密钥健康检查、metadata、轮换、禁用、撤销。
- 为高危操作申请短期凭证。
Heicode 不做:
- 在数据库保存明文密钥。
- 把 OpenBao token 交给前端。
- 把 OpenBao 作为普通公网服务暴露。
- 把 Azure Key Vault access token 交给前端。
- 把 Azure Key Vault 管理入口作为普通用户入口暴露。
产品口径:
@@ -86,7 +86,7 @@ Heicode 不传:
- 长期明文密钥。
- CodeGW key 原文。
- OpenBao root token。
- Azure Key Vault 管理凭据。
- 数据库密码。
产品口径:
@@ -117,7 +117,7 @@ Manager 主做:
- 选择模型提供方。
- 配置 CodeGW 渠道。
- 直接访问 OpenBao。
- 直接访问 Azure Key Vault。
- 直接保存长期云密钥。
Manager 不做:
@@ -144,5 +144,5 @@ Heicode 到 Agnet 平台真实部署 API 的完整联调仍可延期。
1. 普通用户只进入 Heicode。
2. 客户端只登录 Heicode。
3. CodeGW 独立服务运行。
4. OpenBao 只通过受控内网或后端访问。
4. Azure Key Vault 只通过 Manager 后端受控身份访问,普通用户不直接进入。
5. Agnet 平台接口只收结构化上下文和 `secret_ref`,不收长期明文密钥。
+1 -1
View File
@@ -104,7 +104,7 @@ macOS 注意:
2. 旧登录入口。
3. 第三方路由登录入口。
4. CodeGW 后台入口。
5. OpenBao 地址或 token。
5. Azure Key Vault 地址、access token 或密钥保管器后台信息。
6. 长期云密钥输入框。
7. 无法解释的个人 API key 自动复制。
+2 -2
View File
@@ -166,7 +166,7 @@
1. 不展示真实密钥。
2. 不进入 CodeGW 管理后台。
3. 不展示 OpenBao token。
3. 不展示 Azure Key Vault access token。
4. 不强调底层配置复杂度,强调用户流程。
5. 如果 Agnet 真实部署尚未联调,明确说当前展示的是 Heicode 侧 payload、manifest 和占位控制面。
@@ -174,4 +174,4 @@
话术:
> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 OpenBao,只在 Heicode 和客户端完成整个开发流程。
> Heicode 是从想法到上线的智能开发工具。用户主要在客户端输入想法和继续推进任务,在 Manager 绑定 Git、文档、SK 和云资源并部署 Agnet。Heicode 生成子 Agnet 团队、角色和权限,密钥进入密钥保管器,子 Agnet 只拿短期凭证。Agnet 在执行中可以调用 SK 工具,最后完成交付与部署;CodeGW 提供模型、余额和用量,Heicode 展示日志、状态和审计。用户不需要进入 CodeGW 后台,也不需要管理 Azure Key Vault,只在 Heicode 和客户端完成整个开发流程。
@@ -38,7 +38,7 @@ Heicode 不以后台菜单作为主体验。建议使用顶部轻量入口和主
- CodeGW 后台。
- 渠道管理。
- 模型供应商配置。
- OpenBao 控制台。
- Azure Key Vault 控制台。
- 模型提供方选择。
- 旧服务入口。
@@ -322,7 +322,7 @@ Heicode 只追问影响执行路径的少量问题:
## 文案规则
- 用“任务上下文”或“资源上下文”,不要把主流程叫成资源管理。
- 用“密钥保管器”,不用“OpenBao”作为普通用户标题。
- 用“密钥保管器”,不要用具体实现名作为普通用户标题。
- 用“客户端”,不用“本地模型提供方”。
- 用“模型与余额”,不用“CodeGW 后台”。
- 用“执行计划”,不用“控制面草案”。
@@ -334,7 +334,7 @@ Heicode 会复用之前的项目上下文、资源绑定和 Agnet 角色,生
## 口述时不要说
1. 让用户去 CodeGW 后台配置模型。
2. 让用户去 OpenBao 控制台管理密钥。
2. 让用户去 Azure Key Vault 控制台管理密钥。
3. 让客户端选择模型提供方。
4. 让用户手写复杂 manifest。
5. 把 tenant/project 当作当前产品主轴。
@@ -256,7 +256,9 @@ Heicode 通过服务端调用 CodeGW,并把普通用户需要的信息展示
### 5.5 密钥保管器
密钥保管器用于保存长期凭证。当前技术实现是 OpenBao。
密钥保管器用于保存长期凭证。当前技术实现是 Azure Key Vault。
生产环境中 Manager 使用 VM Managed Identity 访问 Azure Key Vault。用户绑定资源后,Manager 将敏感凭证写入 Key Vault,并在 Heicode 数据库中只保存 `azkv://<vault>/secrets/<name>` 形式的 `secret_ref`。
它负责:
@@ -334,7 +336,7 @@ Heicode 判断当前任务需要哪些上下文
用户不需要直接使用:
- CodeGW 后台。
- OpenBao 控制台。
- Azure Key Vault 控制台。
- Agnet 平台后台。
用户不应该看到:
@@ -342,7 +344,7 @@ Heicode 判断当前任务需要哪些上下文
- 模型提供方选择。
- 旧服务入口。
- 明文密钥。
- OpenBao token。
- Azure Key Vault access token。
- CodeGW 管理员配置。
## 十一、常见问题
+2 -2
View File
@@ -14,7 +14,7 @@
| [`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、OpenBao、Agnet 平台边界说明 |
| [`07-integration-boundaries.md`](./07-integration-boundaries.md) | CodeGW、Azure Key Vault、Agnet 平台边界说明 |
| [`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 和客户端页面、组件、状态、文案规则 |
@@ -37,7 +37,7 @@ Heicode 是一款面向全流程智能开发的 SaaS Code 工具。用户注册
- `Heicode Manager` 是浏览器里的辅助控制台,负责账号与安全、客户端下载、Git/云资源绑定、Agnet 部署、任务状态、余额与审计。
- 继续开发时,Heicode 会持续调用 Agnet 推进需求、开发、测试、修复和部署等子环节。
- Agnet 在执行过程中可以调用已授权的 SK 工具,最终完成交付与部署并把结果回传给客户端和 Manager。
- 用户侧叫 `密钥保管器`,技术实现可以说明是 OpenBao。
- 用户侧叫 `密钥保管器`,技术实现可以说明是 Azure Key Vault;Manager 通过 VM Managed Identity 访问,不要求普通用户进入 Key Vault 后台。
- 用户侧叫 `资源绑定`,不要叫 Git 来源。
- 客户端不出现 `模型提供方`、`旧服务入口`、`第三方路由入口`。
- CodeGW 是内部模型网关和计费服务,不作为普通用户产品入口。
+6
View File
@@ -9,7 +9,13 @@ build
*.db-journal
logs
web/default/dist
!web/default/dist/
web/default/dist/*
!web/default/dist/index.html
web/classic/dist
!web/classic/dist/
web/classic/dist/*
!web/classic/dist/index.html
web/node_modules
web/dist
.env
+1 -1
View File
@@ -1 +1 @@
1.4.2
1.4.3
@@ -91,7 +91,7 @@ func baseAgnetResourceGrantPlan() agnetOrchestrationPlan {
"repo_url": "https://example.invalid/acme/project.git",
},
Status: agnetGrantStatusActive,
SecretRef: "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main",
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main",
Audit: map[string]string{
"created_by": "manager-test",
},
@@ -162,7 +162,7 @@ func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) {
require.Equal(t, "https://example.invalid/acme/project.git#main", grants[0].BindingScope)
require.Equal(t, "builder", grants[0].TargetRole)
require.Equal(t, "agent-builder-1", grants[0].TargetAgentRef)
require.Equal(t, "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main", grants[0].SecretRef)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", grants[0].SecretRef)
require.Empty(t, grants[1].SecretRef, "project document grants should not require credential material")
require.Equal(t, "newapi", stored.Plan.BillingContext.Provider)
require.Equal(t, "channel-p1", stored.Plan.UserContext.ChannelID)
@@ -176,7 +176,7 @@ func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) {
require.Equal(t, "user-p1", stored.ResourceGrantManifest.UserID)
require.Equal(t, "builder", stored.ResourceGrantManifest.AgentRole)
require.Equal(t, "agent-builder-1", stored.ResourceGrantManifest.TargetAgentRef)
require.Equal(t, "vault://secret/users/user-p1/bindings/project-main/resources/res-git-main", stored.ResourceGrantManifest.ResourceGrants[0].SecretRef)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/user-p1-project-main-res-git-main", stored.ResourceGrantManifest.ResourceGrants[0].SecretRef)
}
func TestAgnetPermissionManifestExcludesInactiveResourceGrants(t *testing.T) {
@@ -0,0 +1,311 @@
package controller
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
"gorm.io/gorm"
)
type azureResourceDiscoveryClient struct {
authorityHost string
armBaseURL string
client *http.Client
}
type azureARMResource struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Location string `json:"location"`
Tags map[string]any `json:"tags"`
}
type azureARMResourceListResponse struct {
Value []azureARMResource `json:"value"`
NextLink string `json:"nextLink"`
}
func newAzureResourceDiscoveryClientFromEnv() azureResourceDiscoveryClient {
authorityHost := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_AUTHORITY_HOST")), "/")
if authorityHost == "" {
authorityHost = "https://login.microsoftonline.com"
}
armBaseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_ARM_BASE_URL")), "/")
if armBaseURL == "" {
armBaseURL = "https://management.azure.com"
}
return azureResourceDiscoveryClient{
authorityHost: authorityHost,
armBaseURL: armBaseURL,
client: &http.Client{Timeout: 20 * time.Second},
}
}
func DiscoverAzureResources(c *gin.Context) {
userId := c.GetInt("id")
var account model.ResourceBinding
if err := model.DB.Where("id = ? AND user_id = ?", c.Param("id"), userId).First(&account).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "resource not found")
return
}
common.ApiError(c, err)
return
}
if account.ResourceType != "cloud_account" || strings.ToLower(account.Provider) != "azure" {
common.ApiErrorMsg(c, "resource must be an active Azure cloud_account")
return
}
if account.Status != "active" {
common.ApiErrorMsg(c, "resource must be an active Azure cloud_account")
return
}
secretClient, err := newSecretStoreClientFromEnv()
if err != nil {
common.ApiError(c, err)
return
}
secretData, err := secretClient.getJSONSecret(account.SecretRef)
if err != nil {
common.ApiError(c, err)
return
}
metadata := unmarshalResourceJSON(account.Metadata)
credentials := azureDiscoveryCredentials{
SubscriptionID: firstString(metadata, secretData, account.ExternalId, "subscription_id"),
TenantID: firstString(metadata, secretData, account.TenantId, "tenant_id"),
ClientID: firstString(metadata, secretData, "", "client_id"),
ClientSecret: firstString(nil, secretData, "", "client_secret"),
}
if err := credentials.validate(); err != nil {
common.ApiError(c, err)
return
}
azureClient := newAzureResourceDiscoveryClientFromEnv()
token, err := azureClient.clientCredentialsToken(credentials)
if err != nil {
common.ApiError(c, err)
return
}
discovered, err := azureClient.listSubscriptionResources(credentials.SubscriptionID, token)
if err != nil {
common.ApiError(c, err)
return
}
items, err := upsertDiscoveredAzureResources(account, credentials, discovered)
if err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, gin.H{
"account": resourceToResponse(account),
"items": items,
"discovered": len(items),
})
}
type azureDiscoveryCredentials struct {
SubscriptionID string
TenantID string
ClientID string
ClientSecret string
}
func (c azureDiscoveryCredentials) validate() error {
if strings.TrimSpace(c.SubscriptionID) == "" {
return errors.New("Azure subscription_id required")
}
if strings.TrimSpace(c.TenantID) == "" {
return errors.New("Azure tenant_id required")
}
if strings.TrimSpace(c.ClientID) == "" {
return errors.New("Azure client_id required")
}
if strings.TrimSpace(c.ClientSecret) == "" {
return errors.New("Azure client_secret required")
}
return nil
}
func firstString(primary map[string]any, secondary map[string]any, fallback string, key string) string {
if value := mapString(primary, key); value != "" {
return value
}
if value := mapString(secondary, key); value != "" {
return value
}
return strings.TrimSpace(fallback)
}
func mapString(values map[string]any, key string) string {
if values == nil {
return ""
}
switch value := values[key].(type) {
case string:
return strings.TrimSpace(value)
case fmt.Stringer:
return strings.TrimSpace(value.String())
default:
return ""
}
}
func (a azureResourceDiscoveryClient) clientCredentialsToken(credentials azureDiscoveryCredentials) (string, error) {
endpoint := fmt.Sprintf("%s/%s/oauth2/v2.0/token", a.authorityHost, url.PathEscape(credentials.TenantID))
form := url.Values{}
form.Set("grant_type", "client_credentials")
form.Set("client_id", credentials.ClientID)
form.Set("client_secret", credentials.ClientSecret)
form.Set("scope", "https://management.azure.com/.default")
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(form.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := a.client.Do(req)
if err != nil {
return "", fmt.Errorf("Azure client credential token request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
message := readSecretStoreError(resp.Body)
if message == "" {
message = resp.Status
}
return "", fmt.Errorf("Azure client credential token request failed: %s", message)
}
var payload struct {
AccessToken string `json:"access_token"`
}
if err := common.DecodeJson(resp.Body, &payload); err != nil {
return "", err
}
if strings.TrimSpace(payload.AccessToken) == "" {
return "", errors.New("Azure client credential token response missing access_token")
}
return payload.AccessToken, nil
}
func (a azureResourceDiscoveryClient) listSubscriptionResources(subscriptionID string, token string) ([]azureARMResource, error) {
endpoint := fmt.Sprintf("%s/subscriptions/%s/resources?api-version=2021-04-01", a.armBaseURL, url.PathEscape(subscriptionID))
var out []azureARMResource
for page := 0; page < 20 && endpoint != ""; page++ {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := a.client.Do(req)
if err != nil {
return nil, fmt.Errorf("Azure ARM resources request failed: %w", err)
}
var payload azureARMResourceListResponse
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
err = common.DecodeJson(resp.Body, &payload)
} else {
message := readSecretStoreError(resp.Body)
if message == "" {
message = resp.Status
}
err = fmt.Errorf("Azure ARM resources request failed: %s", message)
}
resp.Body.Close()
if err != nil {
return nil, err
}
out = append(out, payload.Value...)
endpoint = strings.TrimSpace(payload.NextLink)
}
return out, nil
}
func upsertDiscoveredAzureResources(account model.ResourceBinding, credentials azureDiscoveryCredentials, discovered []azureARMResource) ([]resourceResponse, error) {
items := make([]resourceResponse, 0, len(discovered))
for _, armResource := range discovered {
if strings.TrimSpace(armResource.ID) == "" {
continue
}
metadata := map[string]any{
"subscription_id": credentials.SubscriptionID,
"tenant_id": credentials.TenantID,
"source_account_id": account.Id,
"type": armResource.Type,
"location": armResource.Location,
"resource_group": azureResourceGroupFromID(armResource.ID),
"tags": armResource.Tags,
}
metadataJSON, err := marshalResourceJSON(metadata)
if err != nil {
return nil, err
}
permissionScopeJSON, err := marshalResourceJSON(map[string]any{"actions": []string{"azure:read"}})
if err != nil {
return nil, err
}
constraints := unmarshalResourceJSON(account.Constraints)
constraintsJSON, err := marshalResourceJSON(constraints)
if err != nil {
return nil, err
}
name := strings.TrimSpace(armResource.Name)
if name == "" {
name = armResource.ID
}
bindingScope := fmt.Sprintf("azure:%s:%s", credentials.SubscriptionID, armResource.ID)
var resource model.ResourceBinding
err = model.DB.Where(
"user_id = ? AND resource_type = ? AND provider = ? AND external_id = ?",
account.UserId,
"cloud_resource",
"azure",
armResource.ID,
).First(&resource).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
resource.UserId = account.UserId
resource.TenantId = account.TenantId
resource.ProjectId = account.ProjectId
resource.BindingScope = bindingScope
resource.Name = name
resource.ResourceType = "cloud_resource"
resource.Provider = "azure"
resource.ExternalId = armResource.ID
resource.SecretRef = account.SecretRef
resource.Metadata = metadataJSON
resource.PermissionScope = permissionScopeJSON
resource.Constraints = constraintsJSON
resource.Status = "active"
if resource.Id == 0 {
if err := model.DB.Create(&resource).Error; err != nil {
return nil, err
}
} else if err := model.DB.Save(&resource).Error; err != nil {
return nil, err
}
items = append(items, resourceToResponse(resource))
}
return items, nil
}
func azureResourceGroupFromID(id string) string {
parts := strings.Split(strings.Trim(id, "/"), "/")
for i := 0; i+1 < len(parts); i++ {
if strings.EqualFold(parts[i], "resourceGroups") {
return parts[i+1]
}
}
return ""
}
+40 -13
View File
@@ -1,6 +1,8 @@
package controller
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
@@ -45,6 +47,9 @@ var secretLikeKeys = map[string]bool{
"token": true,
}
const azureKeyVaultSecretNameMaxLen = 127
const azureKeyVaultSecretNameHashLen = 16
type resourcePayload struct {
TenantId string `json:"tenant_id"`
ProjectId string `json:"project_id"`
@@ -532,12 +537,13 @@ func UpsertResourceSecret(c *gin.Context) {
common.ApiError(c, err)
return
}
secretPath := resourceSecretPath(resource)
if err := client.putKV2(secretPath, payload.Data); err != nil {
secretName := resourceSecretName(resource)
secretRef, err := client.putSecret(secretName, payload.Data)
if err != nil {
common.ApiError(c, err)
return
}
resource.SecretRef = fmt.Sprintf("vault://%s/%s", client.mount, secretPath)
resource.SecretRef = secretRef
if err := model.DB.Save(&resource).Error; err != nil {
common.ApiError(c, err)
return
@@ -548,7 +554,7 @@ func UpsertResourceSecret(c *gin.Context) {
})
}
func resourceSecretPath(resource model.ResourceBinding) string {
func resourceSecretName(resource model.ResourceBinding) string {
scope := resource.BindingScope
if strings.TrimSpace(scope) == "" {
scope = resource.ExternalId
@@ -556,20 +562,17 @@ func resourceSecretPath(resource model.ResourceBinding) string {
if strings.TrimSpace(scope) == "" {
scope = resource.Name
}
return strings.Join([]string{
"users",
fmt.Sprintf("%d", resource.UserId),
"bindings",
return compactAzureSecretName(
fmt.Sprintf("users-%d-bindings-", resource.UserId),
sanitizeSecretPathSegment(scope),
"resources",
fmt.Sprintf("%d", resource.Id),
}, "/")
fmt.Sprintf("-resources-%d", resource.Id),
)
}
func sanitizeSecretPathSegment(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "_"
return "resource"
}
var b strings.Builder
for _, r := range value {
@@ -580,7 +583,7 @@ func sanitizeSecretPathSegment(value string) string {
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '_' || r == '-' || r == '.':
case r == '-':
b.WriteRune(r)
default:
b.WriteRune('-')
@@ -589,6 +592,30 @@ func sanitizeSecretPathSegment(value string) string {
return b.String()
}
func compactAzureSecretName(prefix string, segment string, suffix string) string {
name := prefix + segment + suffix
if len(name) <= azureKeyVaultSecretNameMaxLen {
return name
}
hash := sha256.Sum256([]byte(segment))
hashSuffix := hex.EncodeToString(hash[:])[:azureKeyVaultSecretNameHashLen]
maxSegmentLen := azureKeyVaultSecretNameMaxLen - len(prefix) - len(suffix) - 1 - azureKeyVaultSecretNameHashLen
if maxSegmentLen < 1 {
maxSegmentLen = 1
}
if len(segment) > maxSegmentLen {
segment = segment[:maxSegmentLen]
}
segment = strings.Trim(segment, "-")
if segment == "" {
segment = "resource"
if len(segment) > maxSegmentLen {
segment = segment[:maxSegmentLen]
}
}
return prefix + segment + "-" + hashSuffix + suffix
}
func marshalResourcePayloadJSON(payload resourcePayload) (string, string, string, error) {
metadata, err := marshalResourceJSON(payload.Metadata)
if err != nil {
+179 -29
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
@@ -65,7 +66,7 @@ func TestCreateResourceStoresMetadataAndSecretRefOnly(t *testing.T) {
"resource_type":"git",
"provider":"github",
"external_id":"https://example.com/org/repo",
"secret_ref":"vault://secret/resources/repo",
"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo",
"metadata":{"repo_url":"https://example.com/org/repo","ref":"main","allowed_paths":["."]},
"permission_scope":{"actions":["read","write"]},
"constraints":{"environment":"dev"}
@@ -75,13 +76,13 @@ func TestCreateResourceStoresMetadataAndSecretRefOnly(t *testing.T) {
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"https://example.com/org/repo#main"`)
require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/resources/repo"`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo"`)
var resource model.ResourceBinding
require.NoError(t, db.First(&resource).Error)
require.Equal(t, "git", resource.ResourceType)
require.Equal(t, "https://example.com/org/repo#main", resource.BindingScope)
require.Equal(t, "vault://secret/resources/repo", resource.SecretRef)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo", resource.SecretRef)
require.NotContains(t, resource.Metadata, "token")
require.NotContains(t, resource.PermissionScope, "token")
require.NotContains(t, resource.Constraints, "token")
@@ -93,7 +94,7 @@ func TestCreateResourceRejectsPlaintextSecretKeys(t *testing.T) {
"name":"Cloud account",
"resource_type":"cloud_account",
"metadata":{"account_id":"sub-1","access_key":"do-not-store"},
"secret_ref":"vault://secret/cloud/sub-1"
"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/cloud-sub-1"
}`
w := performResourceRequest(CreateResource, 7, http.MethodPost, "/", body)
@@ -110,7 +111,7 @@ func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
Name: "SK repo",
ResourceType: "sk",
Provider: "git",
SecretRef: "vault://secret/resources/sk-repo",
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/sk-repo",
Metadata: `{"repo_url":"https://example.com/sk.git"}`,
Status: "active",
}
@@ -173,7 +174,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
ResourceType: "git",
Provider: "github",
ExternalId: "https://example.com/org/repo.git",
SecretRef: "vault://secret/resources/repo-main",
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/repo-main",
Metadata: `{"repo_url":"https://example.com/org/repo.git"}`,
PermissionScope: `{"actions":["read","write"]}`,
Status: "active",
@@ -215,11 +216,11 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
require.Contains(t, w.Body.String(), `"target_agent_ref":"agnet-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":"vault://secret/resources/repo-main"`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main"`)
require.NotContains(t, w.Body.String(), `"allowed_actions":["write"]`)
}
func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) {
func TestUpsertResourceSecretWritesAzureKeyVaultAndStoresOnlySecretRef(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
@@ -231,38 +232,50 @@ func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) {
}
require.NoError(t, db.Create(&resource).Error)
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "true", r.Header.Get("Metadata"))
require.Equal(t, "2018-02-01", r.URL.Query().Get("api-version"))
require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource"))
_, _ = w.Write([]byte(`{"access_token":"manager-token","expires_in":"3600"}`))
}))
defer tokenServer.Close()
var writtenPath string
var writtenBody map[string]map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", r.URL.Path)
require.Equal(t, "manager-token", r.Header.Get("X-Vault-Token"))
var writtenBody map[string]any
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPut, r.Method)
require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization"))
require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", r.URL.Path)
require.Equal(t, "7.4", r.URL.Query().Get("api-version"))
require.NoError(t, common.DecodeJson(r.Body, &writtenBody))
writtenPath = r.URL.Path
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"data":{"version":1}}`))
_, _ = w.Write([]byte(`{"id":"https://heicode-kv.vault.azure.net/secrets/users-7-bindings-github-org-repo-main-resources-1/version"}`))
}))
defer server.Close()
t.Setenv("OPENBAO_ADDR", server.URL)
t.Setenv("OPENBAO_TOKEN", "manager-token")
t.Setenv("OPENBAO_KV_MOUNT", "secret")
defer vaultServer.Close()
t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
body := `{"data":{"access_token":"do-not-echo","username":"octo"}}`
w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", body)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"secret_ref":"vault://secret/users/7/bindings/github-org-repo-main/resources/1"`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://127.0.0.1`)
require.Contains(t, w.Body.String(), `/secrets/users-7-bindings-github-org-repo-main-resources-1"`)
require.NotContains(t, w.Body.String(), "do-not-echo")
require.Equal(t, "/v1/secret/data/users/7/bindings/github-org-repo-main/resources/1", writtenPath)
require.Equal(t, "do-not-echo", writtenBody["data"]["access_token"])
require.Equal(t, "/secrets/users-7-bindings-github-org-repo-main-resources-1", writtenPath)
require.Equal(t, "application/json", writtenBody["contentType"])
require.JSONEq(t, `{"access_token":"do-not-echo","username":"octo"}`, writtenBody["value"].(string))
var stored model.ResourceBinding
require.NoError(t, db.First(&stored, resource.Id).Error)
require.Equal(t, "vault://secret/users/7/bindings/github-org-repo-main/resources/1", stored.SecretRef)
require.Contains(t, stored.SecretRef, "azkv://127.0.0.1")
require.Contains(t, stored.SecretRef, "/secrets/users-7-bindings-github-org-repo-main-resources-1")
require.NotContains(t, stored.Metadata, "do-not-echo")
}
func TestUpsertResourceSecretRequiresSecretStoreToken(t *testing.T) {
func TestUpsertResourceSecretRequiresAzureKeyVaultURL(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
@@ -272,14 +285,151 @@ func TestUpsertResourceSecretRequiresSecretStoreToken(t *testing.T) {
Status: "active",
}
require.NoError(t, db.Create(&resource).Error)
t.Setenv("OPENBAO_ADDR", "http://127.0.0.1:8200")
t.Setenv("OPENBAO_TOKEN", "")
t.Setenv("VAULT_TOKEN", "")
t.Setenv("OPENBAO_TOKEN_FILE", "")
t.Setenv("VAULT_TOKEN_FILE", "")
t.Setenv("AZURE_KEY_VAULT_URL", "")
w := performResourceRequestWithRoute(UpsertResourceSecret, 7, http.MethodPost, "/:id/secret", "/1/secret", `{"data":{"token":"x"}}`)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":false`)
require.Contains(t, w.Body.String(), "secret store token is not configured")
require.Contains(t, w.Body.String(), "AZURE_KEY_VAULT_URL is not configured")
}
func TestDiscoverAzureResourcesReadsKeyVaultSecretAndStoresCloudResources(t *testing.T) {
db := setupResourceControllerTestDB(t)
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "https://vault.azure.net", r.URL.Query().Get("resource"))
_, _ = w.Write([]byte(`{"access_token":"kv-token"}`))
}))
defer tokenServer.Close()
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "Bearer kv-token", r.Header.Get("Authorization"))
require.Equal(t, "/secrets/azure-sub-1", r.URL.Path)
require.Equal(t, "7.4", r.URL.Query().Get("api-version"))
_, _ = w.Write([]byte(`{"value":"{\"client_secret\":\"sp-secret\"}"}`))
}))
defer vaultServer.Close()
vaultURL, err := url.Parse(vaultServer.URL)
require.NoError(t, err)
authorityServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/tenant-a/oauth2/v2.0/token", r.URL.Path)
require.NoError(t, r.ParseForm())
require.Equal(t, "client_credentials", r.Form.Get("grant_type"))
require.Equal(t, "client-a", r.Form.Get("client_id"))
require.Equal(t, "sp-secret", r.Form.Get("client_secret"))
require.Equal(t, "https://management.azure.com/.default", r.Form.Get("scope"))
_, _ = w.Write([]byte(`{"access_token":"arm-token"}`))
}))
defer authorityServer.Close()
armServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "Bearer arm-token", r.Header.Get("Authorization"))
require.Equal(t, "/subscriptions/sub-a/resources", r.URL.Path)
require.Equal(t, "2021-04-01", r.URL.Query().Get("api-version"))
_, _ = w.Write([]byte(`{
"value": [
{
"id": "/subscriptions/sub-a/resourceGroups/rg-app/providers/Microsoft.Compute/virtualMachines/vm-api-01",
"name": "vm-api-01",
"type": "Microsoft.Compute/virtualMachines",
"location": "eastus",
"tags": {"env": "test"}
}
]
}`))
}))
defer armServer.Close()
t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
t.Setenv("AZURE_AUTHORITY_HOST", authorityServer.URL)
t.Setenv("AZURE_ARM_BASE_URL", armServer.URL)
account := model.ResourceBinding{
UserId: 7,
BindingScope: "azure:sub-a",
Name: "Test Azure",
ResourceType: "cloud_account",
Provider: "azure",
ExternalId: "sub-a",
SecretRef: fmt.Sprintf("azkv://%s/secrets/azure-sub-1", vaultURL.Host),
Metadata: `{"subscription_id":"sub-a","tenant_id":"tenant-a","client_id":"client-a"}`,
Status: "active",
}
require.NoError(t, db.Create(&account).Error)
w := performResourceRequestWithRoute(
DiscoverAzureResources,
7,
http.MethodPost,
"/:id/azure/discover",
"/1/azure/discover",
"",
)
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"resource_group":"rg-app"`)
require.Contains(t, w.Body.String(), `"type":"Microsoft.Compute/virtualMachines"`)
require.NotContains(t, w.Body.String(), "sp-secret")
var resources []model.ResourceBinding
require.NoError(t, db.Where("user_id = ? AND resource_type = ?", 7, "cloud_resource").Find(&resources).Error)
require.Len(t, resources, 1)
require.Equal(t, "vm-api-01", resources[0].Name)
require.Equal(t, "azure", resources[0].Provider)
require.Equal(t, "active", resources[0].Status)
require.Equal(t, account.SecretRef, resources[0].SecretRef)
require.Contains(t, resources[0].ExternalId, "Microsoft.Compute/virtualMachines/vm-api-01")
require.Contains(t, resources[0].Metadata, `"resource_group":"rg-app"`)
require.NotContains(t, resources[0].Metadata, "sp-secret")
}
func TestResourceSecretNameFitsAzureKeyVaultRules(t *testing.T) {
name := resourceSecretName(model.ResourceBinding{
Id: 99,
UserId: 7,
BindingScope: strings.Repeat("repo/with spaces/", 20),
Name: "fallback",
})
require.LessOrEqual(t, len(name), azureKeyVaultSecretNameMaxLen)
require.Regexp(t, `^[A-Za-z0-9-]+$`, name)
require.Contains(t, name, "users-7-bindings-")
require.Contains(t, name, "-resources-99")
}
func TestGetSecretStoreStatusReportsAzureKeyVaultReachable(t *testing.T) {
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "true", r.Header.Get("Metadata"))
_, _ = w.Write([]byte(`{"access_token":"manager-token"}`))
}))
defer tokenServer.Close()
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "/secrets", r.URL.Path)
require.Equal(t, "Bearer manager-token", r.Header.Get("Authorization"))
_, _ = w.Write([]byte(`{"value":[]}`))
}))
defer vaultServer.Close()
t.Setenv("AZURE_KEY_VAULT_URL", vaultServer.URL)
t.Setenv("AZURE_MANAGED_IDENTITY_TOKEN_URL", tokenServer.URL)
r := gin.New()
r.GET("/status", GetSecretStoreStatus)
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/status", nil))
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"configured":true`)
require.Contains(t, w.Body.String(), `"reachable":true`)
require.Contains(t, w.Body.String(), `"provider":"azure_key_vault"`)
require.Contains(t, w.Body.String(), "Azure Key Vault is reachable")
}
+192 -112
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
@@ -16,73 +17,67 @@ import (
)
type secretStoreClient struct {
address string
mount string
token string
vaultURL string
vaultHost string
tokenEndpoint string
clientID string
client *http.Client
}
func newSecretStoreClientFromEnv() (secretStoreClient, error) {
address := strings.TrimRight(strings.TrimSpace(os.Getenv("OPENBAO_ADDR")), "/")
if address == "" {
address = strings.TrimRight(strings.TrimSpace(os.Getenv("VAULT_ADDR")), "/")
vaultURL := strings.TrimRight(strings.TrimSpace(os.Getenv("AZURE_KEY_VAULT_URL")), "/")
if vaultURL == "" {
return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL is not configured")
}
if address == "" {
address = "http://127.0.0.1:8200"
parsed, err := url.Parse(vaultURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL must be an absolute URL")
}
token := strings.TrimSpace(os.Getenv("OPENBAO_TOKEN"))
if token == "" {
token = strings.TrimSpace(os.Getenv("VAULT_TOKEN"))
if parsed.Scheme != "https" && !strings.HasPrefix(parsed.Host, "127.0.0.1") && !strings.HasPrefix(parsed.Host, "localhost") {
return secretStoreClient{}, errors.New("AZURE_KEY_VAULT_URL must use https")
}
if token == "" {
tokenFile := strings.TrimSpace(os.Getenv("OPENBAO_TOKEN_FILE"))
if tokenFile == "" {
tokenFile = strings.TrimSpace(os.Getenv("VAULT_TOKEN_FILE"))
}
if tokenFile != "" {
data, err := os.ReadFile(tokenFile)
if err != nil {
return secretStoreClient{}, fmt.Errorf("failed to read secret store token file: %w", err)
}
token = strings.TrimSpace(string(data))
}
}
if token == "" {
return secretStoreClient{}, errors.New("secret store token is not configured")
}
mount := strings.Trim(strings.TrimSpace(os.Getenv("OPENBAO_KV_MOUNT")), "/")
if mount == "" {
mount = strings.Trim(strings.TrimSpace(os.Getenv("VAULT_KV_MOUNT")), "/")
}
if mount == "" {
mount = "secret"
tokenEndpoint := strings.TrimSpace(os.Getenv("AZURE_MANAGED_IDENTITY_TOKEN_URL"))
if tokenEndpoint == "" {
tokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
}
return secretStoreClient{
address: address,
mount: mount,
token: token,
vaultURL: vaultURL,
vaultHost: parsed.Host,
tokenEndpoint: tokenEndpoint,
clientID: strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID")),
client: &http.Client{Timeout: 10 * time.Second},
}, nil
}
func (s secretStoreClient) putKV2(path string, data map[string]any) error {
func (s secretStoreClient) putSecret(name string, data map[string]any) (string, error) {
if len(data) == 0 {
return errors.New("secret data required")
return "", errors.New("secret data required")
}
body, err := common.Marshal(map[string]any{"data": data})
value, err := common.Marshal(data)
if err != nil {
return err
return "", err
}
url := fmt.Sprintf("%s/v1/%s/data/%s", s.address, s.mount, strings.Trim(path, "/"))
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
body, err := common.Marshal(map[string]any{
"value": string(value),
"contentType": "application/json",
})
if err != nil {
return err
return "", err
}
token, err := s.accessToken()
if err != nil {
return "", err
}
endpoint := fmt.Sprintf("%s/secrets/%s?api-version=7.4", s.vaultURL, url.PathEscape(name))
req, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Vault-Token", s.token)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := s.client.Do(req)
if err != nil {
return err
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
@@ -90,121 +85,206 @@ func (s secretStoreClient) putKV2(path string, data map[string]any) error {
if message == "" {
message = resp.Status
}
return fmt.Errorf("secret store write failed: %s", message)
return "", fmt.Errorf("Azure Key Vault secret write failed: %s", message)
}
return nil
return s.secretRef(name), nil
}
func (s secretStoreClient) secretRef(name string) string {
return fmt.Sprintf("azkv://%s/secrets/%s", s.vaultHost, name)
}
func (s secretStoreClient) getJSONSecret(secretRef string) (map[string]any, error) {
name, err := s.secretNameFromRef(secretRef)
if err != nil {
return nil, err
}
token, err := s.accessToken()
if err != nil {
return nil, err
}
endpoint := fmt.Sprintf("%s/secrets/%s?api-version=7.4", s.vaultURL, url.PathEscape(name))
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
message := readSecretStoreError(resp.Body)
if message == "" {
message = resp.Status
}
return nil, fmt.Errorf("Azure Key Vault secret read failed: %s", message)
}
var payload struct {
Value string `json:"value"`
}
if err := common.DecodeJson(resp.Body, &payload); err != nil {
return nil, err
}
if strings.TrimSpace(payload.Value) == "" {
return nil, errors.New("Azure Key Vault secret value is empty")
}
var data map[string]any
if err := common.UnmarshalJsonStr(payload.Value, &data); err != nil {
return nil, errors.New("Azure Key Vault secret value must be JSON")
}
if len(data) == 0 {
return nil, errors.New("Azure Key Vault secret value is empty")
}
return data, nil
}
func (s secretStoreClient) secretNameFromRef(secretRef string) (string, error) {
parsed, err := url.Parse(strings.TrimSpace(secretRef))
if err != nil || parsed.Scheme != "azkv" || parsed.Host == "" {
return "", errors.New("secret_ref must use azkv://<vault>/secrets/<name>")
}
if !strings.EqualFold(parsed.Host, s.vaultHost) {
return "", errors.New("secret_ref vault does not match configured Azure Key Vault")
}
parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
if len(parts) < 2 || parts[0] != "secrets" || strings.TrimSpace(parts[1]) == "" {
return "", errors.New("secret_ref must use azkv://<vault>/secrets/<name>")
}
return parts[1], nil
}
func (s secretStoreClient) accessToken() (string, error) {
u, err := url.Parse(s.tokenEndpoint)
if err != nil {
return "", fmt.Errorf("invalid Azure managed identity token endpoint: %w", err)
}
q := u.Query()
if q.Get("api-version") == "" {
q.Set("api-version", "2018-02-01")
}
if q.Get("resource") == "" {
q.Set("resource", "https://vault.azure.net")
}
if s.clientID != "" && q.Get("client_id") == "" {
q.Set("client_id", s.clientID)
}
u.RawQuery = q.Encode()
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return "", err
}
req.Header.Set("Metadata", "true")
resp, err := s.client.Do(req)
if err != nil {
return "", fmt.Errorf("Azure managed identity token request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
message := readSecretStoreError(resp.Body)
if message == "" {
message = resp.Status
}
return "", fmt.Errorf("Azure managed identity token request failed: %s", message)
}
var payload struct {
AccessToken string `json:"access_token"`
}
if err := common.DecodeJson(resp.Body, &payload); err != nil {
return "", err
}
if strings.TrimSpace(payload.AccessToken) == "" {
return "", errors.New("Azure managed identity token response missing access_token")
}
return payload.AccessToken, nil
}
func readSecretStoreError(body io.Reader) string {
var payload struct {
Errors []string `json:"errors"`
Error struct {
Message string `json:"message"`
Code string `json:"code"`
} `json:"error"`
}
if err := common.DecodeJson(body, &payload); err != nil {
return ""
}
if payload.Error.Message != "" {
if payload.Error.Code != "" {
return payload.Error.Code + ": " + payload.Error.Message
}
return payload.Error.Message
}
return strings.Join(payload.Errors, "; ")
}
// secretStoreHealth is what `GET /v1/sys/health` returns on the
// OpenBao/Vault side. We only pull the public-safe fields — version
// string, initialized/sealed/standby flags, server time. No cluster
// IDs, no auth lease ttls, nothing that could leak operator detail.
type secretStoreHealth struct {
Initialized bool `json:"initialized"`
Sealed bool `json:"sealed"`
Standby bool `json:"standby"`
Version string `json:"version"`
ServerTime int64 `json:"server_time_utc"`
}
// fetchHealth pings the OpenBao /sys/health endpoint. That endpoint
// is intentionally token-less in upstream Vault — it's the standard
// liveness probe — so we don't include X-Vault-Token here. Vault
// returns non-2xx HTTP codes for sealed/uninitialized states by
// design (200=initialized+unsealed, 429=standby, 472=DR secondary,
// 473=performance standby, 501=not initialized, 503=sealed). All of
// those still ship a JSON body with the same fields, so we always
// decode and let the caller interpret the status field separately.
func (s secretStoreClient) fetchHealth() (secretStoreHealth, int, error) {
url := fmt.Sprintf("%s/v1/sys/health", s.address)
req, err := http.NewRequest(http.MethodGet, url, nil)
func (s secretStoreClient) probe() (int, error) {
token, err := s.accessToken()
if err != nil {
return secretStoreHealth{}, 0, err
return 0, err
}
endpoint := s.vaultURL + "/secrets?api-version=7.4&maxresults=1"
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return 0, err
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := s.client.Do(req)
if err != nil {
return secretStoreHealth{}, 0, err
return 0, err
}
defer resp.Body.Close()
var h secretStoreHealth
if err := common.DecodeJson(resp.Body, &h); err != nil {
return secretStoreHealth{}, resp.StatusCode, err
}
return h, resp.StatusCode, nil
_, _ = io.Copy(io.Discard, resp.Body)
return resp.StatusCode, nil
}
// secretStoreStatusResponse is the public-safe envelope rendered by
// the admin status page. Crucially does NOT include the configured
// address (could leak internal network topology), the token, the
// mount path, or any secret names.
// the admin status page. It never includes secret names or values.
type secretStoreStatusResponse struct {
Configured bool `json:"configured"` // env vars present?
Reachable bool `json:"reachable"` // /sys/health responded?
Initialized bool `json:"initialized,omitempty"`
Sealed bool `json:"sealed,omitempty"`
Standby bool `json:"standby,omitempty"`
Version string `json:"version,omitempty"`
Reachable bool `json:"reachable"` // Key Vault data plane responded?
Provider string `json:"provider,omitempty"`
AuthMethod string `json:"auth_method,omitempty"`
Message string `json:"message,omitempty"` // human-readable status
CheckedAt int64 `json:"checked_at"` // unix ms (server clock)
}
// GetSecretStoreStatus returns a sanitized vault health snapshot for
// the admin "密钥保管器" status panel. Admin-only (route is mounted
// under the rootRoute group in api-router.go so non-admins can't even
// reach it). The handler is deliberately tolerant: missing env vars,
// network errors, and sealed-vault responses ALL render usable JSON
// the UI can present — we don't bubble HTTP 500 for any of them, the
// "missing config" / "network error" / "sealed" states are valid
// operating modes the UI must visualise distinctly.
// GetSecretStoreStatus returns a sanitized Azure Key Vault status snapshot.
func GetSecretStoreStatus(c *gin.Context) {
out := secretStoreStatusResponse{
CheckedAt: time.Now().UnixMilli(),
Provider: "azure_key_vault",
AuthMethod: "managed_identity",
}
client, err := newSecretStoreClientFromEnv()
if err != nil {
// Most common case in current production: OPENBAO_TOKEN not
// set. Render as "not configured" instead of a server error.
out.Configured = false
out.Message = "secret store env vars not set (OPENBAO_ADDR / OPENBAO_TOKEN). The vault container may still be running but Manager is not wired up to it yet."
out.Message = err.Error()
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
return
}
out.Configured = true
health, status, err := client.fetchHealth()
status, err := client.probe()
if err != nil {
out.Reachable = false
out.Message = "network error contacting secret store: " + err.Error()
out.Message = "Azure Key Vault probe failed: " + err.Error()
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
return
}
out.Reachable = true
out.Initialized = health.Initialized
out.Sealed = health.Sealed
out.Standby = health.Standby
out.Version = health.Version
// Vault uses non-2xx HTTP for sealed/uninit states — surface a
// short reason so the UI can show a coloured pill without parsing
// the boolean matrix itself.
switch {
case !health.Initialized:
out.Message = fmt.Sprintf("secret store reachable but not initialized (HTTP %d)", status)
case health.Sealed:
out.Message = fmt.Sprintf("secret store reachable but SEALED — operator must unseal (HTTP %d)", status)
case health.Standby:
out.Message = fmt.Sprintf("secret store in standby mode (HTTP %d)", status)
case status == http.StatusForbidden:
out.Message = "Azure Key Vault reachable, but managed identity lacks list permission; secret writes can still work if set permission is granted"
case status == http.StatusUnauthorized:
out.Message = "Azure Key Vault reachable, but managed identity was not authorized"
case status >= http.StatusOK && status < http.StatusMultipleChoices:
out.Message = "Azure Key Vault is reachable"
default:
out.Message = "secret store is healthy"
out.Message = fmt.Sprintf("Azure Key Vault returned HTTP %d", status)
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
}
+5
View File
@@ -11,6 +11,7 @@
# SQL_DSN=postgresql://...@heicode.postgres.database.azure.com:5432/heicode?sslmode=require
# REDIS_CONN_STRING=rediss://:...@heicode.redis.cache.windows.net:6380
# SESSION_SECRET=...
# AZURE_KEY_VAULT_URL=https://<vault-name>.vault.azure.net
# (以及项目要求的其它密钥,见官方 heicode 文档)
#
# 3. 构建 Heicode 定制镜像时与本地 override 一致:
@@ -47,6 +48,10 @@ services:
# Agnet 登录后 JIT 同步:邮箱命中以下白名单则自动提权
- HEICODE_ROOT_EMAILS=${HEICODE_ROOT_EMAILS:-}
- HEICODE_ADMIN_EMAILS=${HEICODE_ADMIN_EMAILS:-}
# Long-lived resource credentials are written to Azure Key Vault via
# the VM managed identity. No static secret-store token is required.
- AZURE_KEY_VAULT_URL=${AZURE_KEY_VAULT_URL:-}
- AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}
networks:
- heicode-network
healthcheck:
+4
View File
@@ -23,6 +23,7 @@ import (
const (
InitialScannerBufferSize = 64 << 10 // 64KB (64*1024)
DefaultMaxScannerBufferSize = 64 << 20 // 64MB (64*1024*1024) default SSE buffer size
DefaultStreamingTimeout = 30 * time.Second
DefaultPingInterval = 15 * time.Second
)
@@ -51,6 +52,9 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon
}()
streamingTimeout := time.Duration(constant.StreamingTimeout) * time.Second
if streamingTimeout <= 0 {
streamingTimeout = DefaultStreamingTimeout
}
var (
stopChan = make(chan bool, 3) // 增加缓冲区避免阻塞
+6 -4
View File
@@ -29,10 +29,11 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/uptime/status", controller.GetUptimeKumaStatus)
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
// M7 — secret-store (OpenBao / Vault) status snapshot for the
// admin "密钥保管器" panel. Admin-only because the response
// reveals seal state + version which we don't want public.
apiRouter.GET("/secret-store/status", middleware.AdminAuth(), controller.GetSecretStoreStatus)
// Secret-store status snapshot. The handler returns only
// sanitized Azure Key Vault config/reachability flags and never
// exposes addresses, tokens, secret names or secret values, so
// resource binding can read it as an authenticated user.
apiRouter.GET("/secret-store/status", middleware.UserAuth(), controller.GetSecretStoreStatus)
apiRouter.GET("/notice", controller.GetNotice)
apiRouter.GET("/user-agreement", controller.GetUserAgreement)
apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy)
@@ -215,6 +216,7 @@ func SetApiRouter(router *gin.Engine) {
resourceRoute.GET("/", controller.ListResources)
resourceRoute.POST("/", controller.CreateResource)
resourceRoute.POST("/:id/secret", controller.UpsertResourceSecret)
resourceRoute.POST("/:id/azure/discover", controller.DiscoverAzureResources)
resourceRoute.PUT("/:id", controller.UpdateResource)
resourceRoute.DELETE("/:id", controller.DeleteResource)
}
+10
View File
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Heicode Manager classic placeholder</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
+3
View File
@@ -9,6 +9,9 @@ lerna-debug.log*
node_modules
dist
!dist/
dist/*
!dist/index.html
dist-ssr
*.local
+107 -57
View File
@@ -8,6 +8,7 @@
"@fontsource-variable/public-sans": "^5.2.7",
"@hookform/resolvers": "^5.2.2",
"@lobehub/icons": "^4.0.3",
"@lobehub/ui": "^5.14.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
@@ -30,6 +31,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@rspack/binding-win32-x64-msvc": "^2.0.0",
"@tailwindcss/postcss": "^4.2.2",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-router": "^1.168.23",
@@ -38,6 +40,7 @@
"@visactor/react-vchart": "^2.0.13",
"@visactor/vchart": "^2.0.13",
"ai": "^6.0.27",
"antd": "^6.4.3",
"auto-skeleton-react": "^1.0.5",
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
@@ -83,6 +86,7 @@
"@tanstack/react-router-devtools": "^1.166.13",
"@tanstack/router-plugin": "^1.167.23",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/hast": "^3.0.4",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -118,7 +122,7 @@
"@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="],
"@ant-design/icons": ["@ant-design/icons@6.1.1", "", { "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q=="],
"@ant-design/icons": ["@ant-design/icons@6.2.3", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg=="],
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
@@ -382,7 +386,7 @@
"@lobehub/icons": ["@lobehub/icons@4.12.0", "", { "dependencies": { "antd-style": "^4.1.0", "lucide-react": "^0.469.0", "polished": "^4.3.1" }, "peerDependencies": { "@lobehub/ui": "^4.3.3", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-DVH7pVzM6wEvua2LXH+Iv10/cLeBbueggMFBHa8IlfQel5u3I6JzuaNXXxj2qJu5QYjUCNL5LTpSWuh4TnuLGw=="],
"@lobehub/ui": ["@lobehub/ui@4.38.4", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.3", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.17", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.0.10", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^3.22.0", "@shikijs/transformers": "^3.22.0", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.6", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.19", "emoji-mart": "^5.6.0", "es-toolkit": "^1.44.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.3", "katex": "^0.16.28", "leva": "^0.10.1", "lucide-react": "^0.563.0", "marked": "^17.0.1", "mermaid": "^11.12.2", "motion": "^12.30.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^14.0.0", "react-error-boundary": "^6.1.0", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.2.0", "shiki": "^3.22.0", "shiki-stream": "^0.1.4", "swr": "^2.4.0", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.48.5" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^4.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-FYQeWkR0CoZCaPqEX9AUGrhaIfkYeuacW2KtV+1GS7eGVjREFNNOAgY5PLk20ZMYV/cRFsn9fNG0rqn9PxChxw=="],
"@lobehub/ui": ["@lobehub/ui@5.15.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.19", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.1.19", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^4.0.2", "@shikijs/transformers": "^4.0.2", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.7", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", "emoji-mart": "^5.6.0", "es-toolkit": "^1.46.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.4", "katex": "^0.16.45", "leva": "^0.10.1", "lucide-react": "^1.11.0", "marked": "^17.0.6", "mermaid": "^11.14.0", "motion": "^12.38.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^15.1.0", "react-error-boundary": "^6.1.1", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.3", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^2.0.1", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.3.0", "shiki": "^4.0.2", "shiki-stream": "^0.1.4", "swr": "^2.4.1", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.49.1" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-zh+xWZiOX/yEuV5Vxp7234bXCu7X5lXzLtrh95DMPkaU22lkLOWAjBT24OV5uroc0YjpmmJNfwDB0VQsAXnrAg=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
@@ -498,9 +502,9 @@
"@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="],
"@pierre/diffs": ["@pierre/diffs@1.1.13", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lnX9Fy5eC+07b8g+D8krC3txOY6LRN5VNR1qr9bph9XEyLxbwwfGN7SFRu4HGozpkDdA76JARgxgWHN+uAihmg=="],
"@pierre/diffs": ["@pierre/diffs@1.2.3", "", { "dependencies": { "@pierre/theme": "1.0.3", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-ul83DHH1yqgGxJAw2tqQm2gDO+oQsaF82ZVocwJYfXAm2FhZyyKPTdtv6jswR4A5eF/ILPjiQxyfScMhQcofbA=="],
"@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="],
"@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="],
"@primer/octicons": ["@primer/octicons@19.23.1", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-CzjGmxkmNhyst6EekrS3SJPdtzgIkUMP/LSJch65y99/kmiFXbO1a+q7zoYe3hnI9NaOM0IN+ydDIbOmd8YqcA=="],
@@ -602,7 +606,7 @@
"@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="],
"@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="],
"@rc-component/cascader": ["@rc-component/cascader@1.15.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw=="],
"@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="],
@@ -612,23 +616,23 @@
"@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="],
"@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="],
"@rc-component/dialog": ["@rc-component/dialog@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A=="],
"@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="],
"@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="],
"@rc-component/form": ["@rc-component/form@1.8.0", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-eUD5KKYnIZWmJwRA0vnyO/ovYUfHGU1svydY1OrqU5fw8Oz9Tdqvxvrlh0wl6xI/EW69dT7II49xpgOWzK3T5A=="],
"@rc-component/form": ["@rc-component/form@1.8.1", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ=="],
"@rc-component/image": ["@rc-component/image@1.8.1", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-JfPCijmMl+EaMvbftsEs/4VHmTyJKsZBh5ujFowSA45i9NTVYS1vuHtgpVV/QrGa27kXwbVOZriffCe/PNKuMw=="],
"@rc-component/image": ["@rc-component/image@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="],
"@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="],
"@rc-component/input": ["@rc-component/input@1.3.1", "", { "dependencies": { "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig=="],
"@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="],
"@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="],
"@rc-component/mentions": ["@rc-component/mentions@1.9.0", "", { "dependencies": { "@rc-component/input": "~1.3.0", "@rc-component/menu": "~1.3.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g=="],
"@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="],
"@rc-component/menu": ["@rc-component/menu@1.3.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA=="],
"@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="],
@@ -636,13 +640,13 @@
"@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="],
"@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="],
"@rc-component/notification": ["@rc-component/notification@2.0.7", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ=="],
"@rc-component/overflow": ["@rc-component/overflow@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw=="],
"@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="],
"@rc-component/picker": ["@rc-component/picker@1.9.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g=="],
"@rc-component/picker": ["@rc-component/picker@1.10.0", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w=="],
"@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="],
@@ -664,25 +668,23 @@
"@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="],
"@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="],
"@rc-component/table": ["@rc-component/table@1.10.0", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w=="],
"@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="],
"@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="],
"@rc-component/tabs": ["@rc-component/tabs@1.9.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg=="],
"@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="],
"@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="],
"@rc-component/tour": ["@rc-component/tour@2.4.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg=="],
"@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="],
"@rc-component/tree": ["@rc-component/tree@1.3.2", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA=="],
"@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="],
"@rc-component/tree-select": ["@rc-component/tree-select@1.9.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w=="],
"@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="],
"@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="],
"@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/util": ["@rc-component/util@1.11.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg=="],
"@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="],
@@ -756,7 +758,7 @@
"@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="],
"@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="],
"@shikijs/transformers": ["@shikijs/transformers@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/types": "4.1.0" } }, "sha512-YbuOcAA3kwqKDU9YSt00dtFLrY5lBXjKU3dWaMATyEyPSqBm9Jqblk/uVICxz7lcjwAHzYaEvIiMWX3mTpogkA=="],
"@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
@@ -1058,7 +1060,7 @@
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
"antd": ["antd@6.3.5", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.0", "@rc-component/image": "~1.8.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.1", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.4", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.10.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8BPz9lpZWQm42PTx7yL4KxWAotVuqINiKcoYRcLtdd5BFmAcAZicVyFTnBJyRDlzGZFZeRW3foGu6jXYFnej6Q=="],
"antd": ["antd@6.4.3", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.2.3", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.29.2", "@rc-component/cascader": "~1.15.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.9.0", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.1", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.3.0", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.9.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~2.0.7", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.10.0", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.10.0", "@rc-component/tabs": "~1.9.0", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.3.1", "@rc-component/tree-select": "~1.9.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw=="],
"antd-style": ["antd-style@4.1.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.0", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=6.0.0", "react": ">=18" } }, "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ=="],
@@ -1212,7 +1214,7 @@
"d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
"d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
@@ -1360,7 +1362,7 @@
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
"es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
@@ -1856,9 +1858,9 @@
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="],
"micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="],
"micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="],
"micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
@@ -2132,7 +2134,7 @@
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"react-avatar-editor": ["react-avatar-editor@14.0.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NaQM3oo4u0a1/Njjutc2FjwKX35vQV+t6S8hovsbAlMpBN1ntIwP/g+Yr9eDIIfaNtRXL0AqboTnPmRxhD/i8A=="],
"react-avatar-editor": ["react-avatar-editor@15.1.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="],
"react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="],
@@ -2210,7 +2212,7 @@
"remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="],
"remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="],
"remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
@@ -2512,7 +2514,7 @@
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"virtua": ["virtua@0.48.8", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-jpsxOw5V4B6hg44JePRLo9DL0TV7N1lBEVtPjKpAJebXyhI2s9lfiXJESaLapNtr3vtiSk/pWHiLf7B2a6UcgQ=="],
"virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="],
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
@@ -2574,6 +2576,10 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@ant-design/cssinjs/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@ant-design/cssinjs-utils/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@@ -2598,20 +2604,20 @@
"@eslint/config-array/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"@lobehub/fluent-emoji/es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
"@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
"@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
"@lobehub/ui/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@lobehub/ui/lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
"@lobehub/ui/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
"@lobehub/ui/lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="],
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="],
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
@@ -2644,21 +2650,69 @@
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@rc-component/checkbox/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/collapse/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/color-picker/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/context/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/dialog/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
"@rc-component/drawer/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
"@rc-component/drawer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/dropdown/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/image/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
"@rc-component/input-number/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/motion/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/mutate-observer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/overflow/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/pagination/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/progress/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/rate/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/resize-observer/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/segmented/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/select/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/slider/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/steps/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/switch/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/tooltip/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/tour/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
"@rc-component/tree/@rc-component/virtual-list": ["@rc-component/virtual-list@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ=="],
"@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="],
"@rc-component/trigger/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/upload/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/virtual-list/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@shikijs/transformers/@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
"@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
@@ -2706,26 +2760,22 @@
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
"d3/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
"d3/d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
"d3-contour/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-dsv/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"d3-dsv/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"d3-fetch/d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
"d3-geo/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
"d3-sankey/d3-array": ["d3-array@1.2.4", "", {}, "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="],
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
"d3-scale/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-time/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"eslint/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
@@ -2822,20 +2872,12 @@
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
"@lobehub/ui/@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@lobehub/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
"@lobehub/ui/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
"@lobehub/ui/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
"@lobehub/ui/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
"@lobehub/ui/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
@@ -2848,6 +2890,14 @@
"@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@rc-component/dialog/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/image/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@rc-component/tour/@rc-component/portal/@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="],
"@shikijs/transformers/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="],
"@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
+10
View File
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Heicode Manager placeholder</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
+2 -2
View File
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react'
import { ShieldOff, WifiOff } from 'lucide-react'
import { ShieldOff, WifiOff, type LucideIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { EmptyState } from './empty-state'
import { ErrorState } from './error-state'
@@ -41,7 +41,7 @@ interface QueryStateProps {
loadingFallback?: ReactNode
emptyTitle?: string
emptyDescription?: string
emptyIcon?: React.ComponentType<{ className?: string }>
emptyIcon?: LucideIcon
emptyAction?: ReactNode
}
@@ -1,7 +1,14 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { AlertTriangle, Cloud, ShieldCheck } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import {
createManagerResource,
discoverManagerAzureResources,
putManagerResourceSecret,
updateManagerResource,
} from '@/lib/manager-resources'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -12,28 +19,21 @@ import {
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { toast } from 'sonner'
import { createResource } from '@/lib/heicode-mcp'
// M2 phase 1 — Azure cloud account binding via manual Service Principal
// credentials. Phase 2 (real OAuth code flow) and phase 3 (ARM resource
// auto-discovery) require Azure App Registration + OpenBao to be online
// first; until then, operators paste SP creds manually here. The sheet
// is deliberately explicit about the security caveat: when OpenBao
// isn't configured, the client_secret would be stored alongside the
// resource binding, NOT in a vault. Caller sees a yellow warning until
// vault is wired up.
// Azure cloud account binding via manual Service Principal credentials.
// The Manager creates a local ResourceBinding first, writes the
// client_secret to Azure Key Vault via /api/resources/:id/secret, stores
// only azkv://... on the resource row, then asks Manager to enumerate ARM
// resources and persist them as cloud_resource bindings.
//
// Field map → mcp-server ResourceBinding:
// type = 'cloud_account'
// Field map → Manager ResourceBinding:
// resource_type = 'cloud_account'
// name = display_name
// external_ref = subscription_id
// metadata = { provider, tenant_id, client_id }
// secret_ref = vault://<TBD> (intentionally empty during phase 1;
// server should refuse to materialise creds until
// OpenBao is wired up)
// constraints = {} (resource group filters land in phase 3)
// permission_scope= ['azure:read'] (read-only ARM access scope)
// external_id = subscription_id
// metadata = { subscription_id, tenant_id, client_id }
// secret_ref = azkv://<vault>/secrets/<name>
// constraints = {} (resource group filters can be added later)
// permission_scope= { actions: ['azure:read'] }
interface Props {
open: boolean
@@ -41,7 +41,11 @@ interface Props {
vaultConfigured?: boolean
}
export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }: Props) {
export function AzureCloudBindingSheet({
open,
onOpenChange,
vaultConfigured,
}: Props) {
const { t } = useTranslation()
const qc = useQueryClient()
@@ -54,28 +58,68 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
})
const mutation = useMutation({
mutationFn: () =>
createResource({
type: 'cloud_account',
mutationFn: async () => {
if (!vaultConfigured) {
throw new Error(t('Azure Key Vault is not ready'))
}
const pending = await createManagerResource({
resource_type: 'cloud_account',
name: form.name.trim(),
external_ref: form.subscription_id.trim(),
metadata: {
provider: 'azure',
external_id: form.subscription_id.trim(),
binding_scope: `azure:${form.subscription_id.trim()}`,
metadata: {
subscription_id: form.subscription_id.trim(),
tenant_id: form.tenant_id.trim(),
client_id: form.client_id.trim(),
// NOTE: client_secret is NOT included here. We rely on the
// server side to mint a secret_ref via Secret Broker when
// vault is online; until then the binding is created with
// empty secret_ref and a "needs vault" status. Phase 2
// wires up the actual cred-write step.
},
permission_scope: ['azure:read'],
permission_scope: { actions: ['azure:read'] },
constraints: {},
status: vaultConfigured ? 'active' : 'pending',
}),
onSuccess: () => {
status: 'pending',
})
const secret = await putManagerResourceSecret(pending.id, {
subscription_id: form.subscription_id.trim(),
tenant_id: form.tenant_id.trim(),
client_id: form.client_id.trim(),
client_secret: form.client_secret,
})
const account = await updateManagerResource(pending.id, {
resource_type: 'cloud_account',
name: pending.name,
provider: pending.provider,
external_id: pending.external_id,
binding_scope: pending.binding_scope,
secret_ref: secret.secret_ref,
metadata: pending.metadata,
permission_scope: pending.permission_scope,
constraints: pending.constraints,
status: 'active',
})
try {
const discovery = await discoverManagerAzureResources(account.id)
return { account, discovery, discoveryError: '' }
} catch (err) {
return {
account,
discovery: null,
discoveryError:
err instanceof Error ? err.message : t('Azure discovery failed'),
}
}
},
onSuccess: (result) => {
void qc.invalidateQueries({ queryKey: ['heicode', 'resources'] })
void qc.invalidateQueries({ queryKey: ['manager', 'cloud-resources'] })
toast.success(t('Azure subscription bound'))
if (result.discoveryError) {
toast.warning(result.discoveryError)
} else if (result.discovery) {
toast.success(
t('{{n}} Azure resources discovered', {
n: result.discovery.discovered,
})
)
}
onOpenChange(false)
setForm({
name: '',
@@ -86,7 +130,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
})
},
onError: (err) => {
toast.error(err instanceof Error ? err.message : t('Failed to bind Azure account'))
toast.error(
err instanceof Error ? err.message : t('Failed to bind Azure account')
)
},
})
@@ -95,19 +141,20 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
form.subscription_id.trim() !== '' &&
form.tenant_id.trim() !== '' &&
form.client_id.trim() !== '' &&
form.client_secret.trim() !== ''
form.client_secret.trim() !== '' &&
Boolean(vaultConfigured)
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className='w-[min(560px,96vw)] sm:max-w-none overflow-y-auto'>
<SheetContent className='w-[min(560px,96vw)] overflow-y-auto sm:max-w-none'>
<SheetHeader>
<SheetTitle className='flex items-center gap-2'>
<Cloud className='h-5 w-5 text-primary' />
<Cloud className='text-primary h-5 w-5' />
{t('Connect Azure subscription')}
</SheetTitle>
<SheetDescription>
{t(
'Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage) once auto-discovery lands. AWS and GCP are coming soon.'
'Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage). AWS and GCP are coming soon.'
)}
</SheetDescription>
</SheetHeader>
@@ -118,9 +165,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
<AlertTriangle className='h-3.5 w-3.5' />
{t('Secret vault not yet configured')}
</p>
<p className='mt-1.5 text-muted-foreground'>
<p className='text-muted-foreground mt-1.5'>
{t(
'OpenBao is not wired up to the Manager yet (see System settings → Secret vault status). The binding will be created in pending state — the client_secret will NOT be persisted server-side until vault is configured. Re-enter or rotate the secret once vault is online.'
'Azure Key Vault is not ready yet (see System settings → Secret vault status). The subscription cannot be bound until Manager can write the client_secret to Key Vault.'
)}
</p>
</div>
@@ -165,7 +212,9 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
</div>
<div className='grid gap-3 sm:grid-cols-2'>
<div>
<Label htmlFor='az-client'>{t('Client ID (Application ID)')}</Label>
<Label htmlFor='az-client'>
{t('Client ID (Application ID)')}
</Label>
<Input
id='az-client'
value={form.client_id}
@@ -191,16 +240,29 @@ export function AzureCloudBindingSheet({ open, onOpenChange, vaultConfigured }:
</div>
</div>
<div className='mt-2 rounded-lg border border-dashed bg-card/40 p-3 text-xs text-muted-foreground'>
<p className='flex items-center gap-1.5 font-medium text-foreground'>
<ShieldCheck className='h-3.5 w-3.5 text-primary' />
<div className='bg-card/40 text-muted-foreground mt-2 rounded-lg border border-dashed p-3 text-xs'>
<p className='text-foreground flex items-center gap-1.5 font-medium'>
<ShieldCheck className='text-primary h-3.5 w-3.5' />
{t('What Heicode will and will not do')}
</p>
<ul className='mt-2 space-y-1'>
<li>· {t('Read-only ARM access (Reader role recommended)')}</li>
<li>· {t('Auto-discover VM / database / AKS / storage — coming soon')}</li>
<li>· {t('Never modify resources without explicit approval from the desktop client')}</li>
<li>· {t('Never write the client_secret into Manager logs or audit payloads')}</li>
<li>
·{' '}
{t('Auto-discover VM / database / AKS / storage')}
</li>
<li>
·{' '}
{t(
'Never modify resources without explicit approval from the desktop client'
)}
</li>
<li>
·{' '}
{t(
'Never write the client_secret into Manager logs or audit payloads'
)}
</li>
</ul>
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -259,6 +259,11 @@ export function SignUpForm({
/>
</div>
</FormControl>
<p className='text-muted-foreground mt-1.5 text-xs leading-relaxed'>
{t(
'We recommend using a mainland China email address so verification emails arrive more reliably.'
)}
</p>
<FormMessage />
</FormItem>
)}
@@ -18,9 +18,8 @@
* 还没接,等接通后这个 stash 流程换成 POST /api/task。
*/
import { useCallback, useMemo, useRef, useState } from 'react'
import { Link, useNavigate } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { Link, useNavigate } from '@tanstack/react-router'
import {
ArrowRight,
ArrowUpRight,
@@ -36,15 +35,16 @@ import {
UserCog,
XCircle,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { toast as sonnerToast } from 'sonner'
import {
createTaskFromIntent,
listHeicodeTasks,
type HeicodeTask,
} from '@/lib/heicode-mcp'
import { toast as sonnerToast } from 'sonner'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
const DRAFT_STORAGE_KEY = 'heicode_idea_draft'
@@ -93,7 +93,7 @@ function StatusBadge({ phase }: { phase: string }) {
return (
<span
className={cn(
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.12em] ring-1 ring-inset',
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-[0.12em] uppercase ring-1 ring-inset',
p.cls
)}
>
@@ -144,10 +144,14 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
setSubmitting(true)
try {
const task = await createTaskFromIntent(trimmed)
sonnerToast.success(t('Task drafted. Answer the follow-ups to build the recommendation.'))
sonnerToast.success(
t('Task drafted. Answer the follow-ups to build the recommendation.')
)
void navigate({ to: '/tasks/$id', params: { id: task.id } })
} catch (err) {
sonnerToast.error(err instanceof Error ? err.message : t('Failed to create task'))
sonnerToast.error(
err instanceof Error ? err.message : t('Failed to create task')
)
} finally {
setSubmitting(false)
}
@@ -183,7 +187,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
>
{t('What idea would you like to turn into shippable software?')}
</h2>
<p className='mt-2 text-sm text-muted-foreground'>
<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.'
)}
@@ -200,7 +204,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
placeholder={t(
'e.g. A task-management SaaS for small teams: login, projects, tasks, comments, notifications, deploy to Azure.'
)}
className='w-full resize-none rounded-2xl border px-4 py-3 text-sm leading-relaxed shadow-[inset_0_1px_0_rgba(123,107,227,0.10)] outline-none transition focus:ring-2 focus:ring-[color-mix(in_oklch,var(--primary)_40%,transparent)]'
className='w-full resize-none rounded-2xl border px-4 py-3 text-sm leading-relaxed shadow-[inset_0_1px_0_rgba(123,107,227,0.10)] transition outline-none focus:ring-2 focus:ring-[color-mix(in_oklch,var(--primary)_40%,transparent)]'
style={{
borderColor: 'rgba(123,107,227,0.22)',
backgroundColor: 'rgba(255,255,255,0.04)',
@@ -214,7 +218,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
/>
<div className='flex flex-wrap items-center justify-between gap-3'>
<p className='text-xs text-muted-foreground'>
<p className='text-muted-foreground text-xs'>
{t(
'Heicode Manager only captures the idea. The main task conversation happens in the desktop client.'
)}
@@ -222,6 +226,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
<Button
type='button'
onClick={handleSubmit}
disabled={submitting}
className='h-10 gap-2 rounded-xl px-5 text-sm font-semibold text-white shadow-[0_18px_48px_-18px_rgba(123,107,227,0.65)] transition-transform hover:translate-y-[-1px] active:translate-y-0 active:scale-[0.99]'
style={{
backgroundImage: 'var(--gradient-brand-btn)',
@@ -229,7 +234,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
}}
>
<Sparkles className='h-4 w-4' />
{t('Save idea')}
{submitting ? t('Saving') : t('Save idea')}
<span className='ms-1 hidden text-[10px] font-medium opacity-80 sm:inline'>
{t('⌘/Ctrl + Enter')}
</span>
@@ -250,7 +255,9 @@ function ContinueTasks({
t: ReturnType<typeof useTranslation>['t']
}) {
const recent = useMemo(() => {
const sorted = [...tasks].sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))
const sorted = [...tasks].sort(
(a, b) => (b.updated_at || 0) - (a.updated_at || 0)
)
return sorted.slice(0, 4)
}, [tasks])
@@ -258,7 +265,7 @@ function ContinueTasks({
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_60%,transparent)] p-5'>
<header className='mb-4 flex items-center justify-between'>
<div>
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
{t('Continue working')}
</p>
<h3 className='mt-0.5 text-base font-semibold'>
@@ -269,7 +276,7 @@ function ContinueTasks({
asChild
variant='ghost'
size='sm'
className='gap-1 text-primary'
className='text-primary gap-1'
>
<Link to='/deployments'>
{t('All tasks')}
@@ -284,7 +291,7 @@ function ContinueTasks({
))}
</div>
) : recent.length === 0 ? (
<p className='rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/40 p-4 text-center text-xs text-muted-foreground'>
<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.'
)}
@@ -296,17 +303,17 @@ function ContinueTasks({
<Link
to='/tasks/$id'
params={{ id: task.id }}
className='flex items-start justify-between gap-3 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-3 transition hover:-translate-y-px hover:border-primary/40 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.4)]'
className='bg-background/40 hover:border-primary/40 flex items-start justify-between gap-3 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] p-3 transition hover:-translate-y-px hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.4)]'
>
<div className='min-w-0 flex-1'>
<p className='line-clamp-1 text-sm font-medium'>
{task.name || task.intent || t('Untitled task')}
</p>
<p className='mt-1 text-[11px] text-muted-foreground'>
<p className='text-muted-foreground mt-1 text-[11px]'>
{task.status_caption || task.status}
{' · '}
{t('Updated')}{' '}
{formatRelativeTimeMs(task.updated_at)} {t('ago')}
{t('Updated')} {formatRelativeTimeMs(task.updated_at)}{' '}
{t('ago')}
</p>
</div>
<StatusBadge phase={task.status} />
@@ -329,14 +336,18 @@ function TodayFocus({
t: ReturnType<typeof useTranslation>['t']
}) {
const buckets = useMemo(() => {
const failed = tasks.filter((task) => classifyStatus(task.status) === 'failed')
const failed = tasks.filter(
(task) => classifyStatus(task.status) === 'failed'
)
// "Pending confirmation" maps to HeicodeTask.status = 'configuring'
// (followups not all answered yet) AND tasks awaiting_approval.
const pending = tasks.filter(
(task) =>
task.status === 'configuring' || task.status === 'awaiting_approval'
)
const running = tasks.filter((task) => classifyStatus(task.status) === 'running')
const running = tasks.filter(
(task) => classifyStatus(task.status) === 'running'
)
return { failed, pending, running }
}, [tasks])
@@ -373,14 +384,15 @@ function TodayFocus({
const toneCls: Record<'failed' | 'pending' | 'running' | 'idle', string> = {
failed: 'text-rose-400 bg-rose-500/10 ring-rose-500/25',
pending: 'text-amber-400 bg-amber-500/10 ring-amber-500/25',
running: 'text-primary bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] ring-primary/30',
running:
'text-primary bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] ring-primary/30',
idle: 'text-muted-foreground bg-muted/40 ring-border',
}
return (
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_60%,transparent)] p-5'>
<header className='mb-4'>
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
{t("Today's focus")}
</p>
<h3 className='mt-0.5 text-base font-semibold'>
@@ -398,7 +410,7 @@ function TodayFocus({
{focusItems.map((it) => (
<div
key={it.title}
className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-3'
className='bg-background/40 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] p-3'
>
<div className='flex items-center gap-2'>
<span
@@ -409,12 +421,12 @@ function TodayFocus({
>
<it.Icon className='h-3.5 w-3.5' />
</span>
<p className='text-xs font-medium text-muted-foreground'>
<p className='text-muted-foreground text-xs font-medium'>
{it.title}
</p>
</div>
<p className='mt-2 text-2xl font-semibold'>{it.count}</p>
<p className='mt-0.5 text-[11px] text-muted-foreground'>
<p className='text-muted-foreground mt-0.5 text-[11px]'>
{it.hint}
</p>
</div>
@@ -425,11 +437,7 @@ function TodayFocus({
)
}
function HelperEntries({
t,
}: {
t: ReturnType<typeof useTranslation>['t']
}) {
function HelperEntries({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
const entries: Array<{
Icon: typeof GitBranch
title: string
@@ -477,7 +485,7 @@ function HelperEntries({
return (
<section>
<header className='mb-3'>
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
{t('Helpers')}
</p>
<h3 className='mt-0.5 text-base font-semibold'>
@@ -489,10 +497,10 @@ function HelperEntries({
<Link
key={e.to}
to={e.to}
className='group flex items-start gap-3 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.55)]'
className='group hover:border-primary/40 flex items-start gap-3 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:-translate-y-0.5 hover:shadow-[0_18px_48px_-32px_rgba(123,107,227,0.55)]'
>
<span
className='inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-primary'
className='text-primary inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl'
style={{
backgroundColor: 'rgba(123,107,227,0.12)',
border: '1px solid rgba(123,107,227,0.22)',
@@ -505,7 +513,7 @@ function HelperEntries({
{e.title}
<ArrowRight className='h-3.5 w-3.5 -translate-x-0.5 opacity-0 transition group-hover:translate-x-0 group-hover:opacity-70' />
</p>
<p className='mt-0.5 text-xs text-muted-foreground'>{e.desc}</p>
<p className='text-muted-foreground mt-0.5 text-xs'>{e.desc}</p>
</div>
</Link>
))}
@@ -1,12 +1,12 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CheckCircle2, AlertTriangle, RefreshCw, ShieldOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useTranslation } from 'react-i18next'
import { api } from '@/lib/api'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { SettingsSection } from '../components/settings-section'
// M7 — admin-only secret-store (OpenBao / Vault) status snapshot.
// Secret-store (Azure Key Vault) status snapshot.
// Pure read; deliberately exposes only:
// - configuration state (env vars set?)
// - reachability
@@ -21,10 +21,8 @@ import { SettingsSection } from '../components/settings-section'
type StoreStatus = {
configured: boolean
reachable: boolean
initialized?: boolean
sealed?: boolean
standby?: boolean
version?: string
provider?: string
auth_method?: string
message?: string
checked_at: number
}
@@ -73,9 +71,9 @@ export function SecretStoreSection() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Pick the colour-coded summary pill based on the three operational
// states the backend can return. Order matters: not-configured beats
// reachability, and sealed beats reachable-but-otherwise-fine.
// Pick the colour-coded summary pill based on the operational states
// the backend can return. Order matters: not-configured beats
// reachability.
let toneClass = 'bg-muted/40 text-muted-foreground ring-border/60'
let toneIcon = ShieldOff
let toneLabel = t('Unknown')
@@ -88,14 +86,6 @@ export function SecretStoreSection() {
toneClass = 'bg-rose-500/15 text-rose-400 ring-rose-500/30'
toneIcon = AlertTriangle
toneLabel = t('Unreachable')
} else if (status.sealed) {
toneClass = 'bg-amber-500/15 text-amber-400 ring-amber-500/30'
toneIcon = AlertTriangle
toneLabel = t('Sealed')
} else if (!status.initialized) {
toneClass = 'bg-amber-500/15 text-amber-400 ring-amber-500/30'
toneIcon = AlertTriangle
toneLabel = t('Not initialized')
} else {
toneClass = 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30'
toneIcon = CheckCircle2
@@ -108,10 +98,10 @@ export function SecretStoreSection() {
<SettingsSection
title={t('Secret vault status')}
description={t(
'Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.'
'Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.'
)}
>
<div className='rounded-lg border bg-card/40 p-4'>
<div className='bg-card/40 rounded-lg border p-4'>
<div className='flex items-center justify-between gap-3'>
<span
className={cn(
@@ -130,7 +120,7 @@ export function SecretStoreSection() {
disabled={loading}
>
<RefreshCw
className={cn('h-3.5 w-3.5 me-1', loading && 'animate-spin')}
className={cn('me-1 h-3.5 w-3.5', loading && 'animate-spin')}
/>
{t('Refresh')}
</Button>
@@ -144,36 +134,18 @@ export function SecretStoreSection() {
{status && (
<div className='mt-4 grid gap-3 sm:grid-cols-2'>
<Field label={t('Configured')} value={status.configured ? 'Yes' : 'No'} />
<Field
label={t('Configured')}
value={status.configured ? 'Yes' : 'No'}
/>
<Field label={t('Provider')} value={status.provider || '—'} />
<Field label={t('Auth method')} value={status.auth_method || '—'} />
<Field
label={t('Reachable')}
value={
status.configured ? (status.reachable ? 'Yes' : 'No') : '—'
}
/>
<Field
label={t('Initialized')}
value={
status.reachable
? status.initialized
? 'Yes'
: 'No'
: '—'
}
/>
<Field
label={t('Sealed')}
value={
status.reachable ? (status.sealed ? 'Yes' : 'No') : '—'
}
/>
<Field
label={t('Standby')}
value={
status.reachable ? (status.standby ? 'Yes' : 'No') : '—'
}
/>
<Field label={t('Version')} value={status.version || '—'} />
<Field
label={t('Last checked')}
value={formatTs(status.checked_at)}
@@ -182,15 +154,15 @@ export function SecretStoreSection() {
)}
{status?.message && (
<p className='mt-3 rounded-md border border-dashed border-border/60 bg-background/40 p-2 text-xs text-muted-foreground'>
<p className='border-border/60 bg-background/40 text-muted-foreground mt-3 rounded-md border border-dashed p-2 text-xs'>
{status.message}
</p>
)}
{status && !status.configured && (
<p className='mt-3 text-xs text-muted-foreground'>
<p className='text-muted-foreground mt-3 text-xs'>
{t(
'To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.'
'To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.'
)}
</p>
)}
@@ -202,10 +174,10 @@ export function SecretStoreSection() {
function Field({ label, value }: { label: string; value: string }) {
return (
<div>
<p className='text-[11px] uppercase tracking-[0.1em] text-muted-foreground'>
<p className='text-muted-foreground text-[11px] tracking-[0.1em] uppercase'>
{label}
</p>
<p className='mt-0.5 font-mono text-sm text-foreground'>{value}</p>
<p className='text-foreground mt-0.5 font-mono text-sm'>{value}</p>
</div>
)
}
@@ -81,12 +81,12 @@ const MAINTENANCE_SECTIONS = [
},
},
{
// M7 — secret-store (OpenBao / Vault) status panel. Read-only,
// Secret-store (Azure Key Vault) status panel. Read-only,
// shows seal state + reachability; never exposes secret names
// or plaintext values.
id: 'secret-store',
titleKey: 'Secret vault status',
descriptionKey: 'OpenBao / Vault health and seal state',
descriptionKey: 'Azure Key Vault reachability and managed identity state',
build: (_settings: MaintenanceSettings) => <SecretStoreSection />,
},
{
+6 -6
View File
@@ -1,19 +1,19 @@
import { useState, useEffect, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { getSelf } from '@/lib/api'
import { getHeicodeBalance } from '@/lib/heicode-mcp'
import { useStatus } from '@/hooks/use-status'
import { useSystemConfig } from '@/hooks/use-system-config'
import { getHeicodeBalance } from '@/lib/heicode-mcp'
import { SectionPageLayout } from '@/components/layout'
import { AffiliateRewardsCard } from './components/affiliate-rewards-card'
import { BillingHistoryDialog } from './components/dialogs/billing-history-dialog'
import { CreemConfirmDialog } from './components/dialogs/creem-confirm-dialog'
import { PaymentConfirmDialog } from './components/dialogs/payment-confirm-dialog'
import { TransferDialog } from './components/dialogs/transfer-dialog'
import { HeicodeUsageCard } from './components/heicode-usage-card'
import { RechargeFormCard } from './components/recharge-form-card'
import { SubscriptionPlansCard } from './components/subscription-plans-card'
import { WalletStatsCard } from './components/wallet-stats-card'
import { HeicodeUsageCard } from './components/heicode-usage-card'
import { DEFAULT_DISCOUNT_RATE } from './constants'
import {
useTopupInfo,
@@ -101,14 +101,14 @@ export function Wallet(props: WalletProps) {
setUser({
id: heicodeBalance.heicodeUserId,
username: heicodeBalance.username,
email: heicodeBalance.email,
display_name: heicodeBalance.displayName,
group: heicodeBalance.group,
status: heicodeBalance.status,
quota: heicodeBalance.quota,
used_quota: heicodeBalance.usedQuota,
request_count: heicodeBalance.requestCount,
} as UserWalletData)
aff_quota: 0,
aff_history_quota: 0,
aff_count: 0,
})
return
}
const response = await getSelf()
+13 -8
View File
@@ -3949,6 +3949,7 @@
"WeChat login QR code": "WeChat login QR code",
"WeChat QR code will be displayed here": "WeChat QR code will be displayed here",
"WeChat sign in": "WeChat sign in",
"We recommend using a mainland China email address so verification emails arrive more reliably.": "We recommend using a mainland China email address so verification emails arrive more reliably.",
"Week": "Week",
"Weekday": "Weekday",
"Weekly": "Weekly",
@@ -4120,8 +4121,8 @@
"{{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.",
"Secret vault status": "Secret vault status",
"OpenBao / Vault health and seal state": "OpenBao / Vault health and seal state",
"Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.": "Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.",
"Azure Key Vault reachability and managed identity state": "Azure Key Vault reachability and managed identity state",
"Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.": "Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.",
"Not configured": "Not configured",
"Unreachable": "Unreachable",
"Sealed": "Sealed",
@@ -4131,21 +4132,23 @@
"Failed to read secret store status": "Failed to read secret store status",
"Refresh": "Refresh",
"Configured": "Configured",
"Provider": "Provider",
"Auth method": "Auth method",
"Reachable": "Reachable",
"Initialized": "Initialized",
"Standby": "Standby",
"Version": "Version",
"Last checked": "Last checked",
"To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.": "To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.",
"To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.": "To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.",
"You do not have permission to view this": "You do not have permission to view this",
"Your account or token lacks the required permission. Contact an administrator to request access.": "Your account or token lacks the required permission. Contact an administrator to request access.",
"Network unreachable": "Network unreachable",
"Could not reach the server. Check your internet connection and the Heicode Manager status, then retry.": "Could not reach the server. Check your internet connection and the Heicode Manager status, then retry.",
"Failed to load": "Failed to load",
"Connect Azure subscription": "Connect Azure subscription",
"Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage) once auto-discovery lands. AWS and GCP are coming soon.": "Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage) once auto-discovery lands. AWS and GCP are coming soon.",
"Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage). AWS and GCP are coming soon.": "Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage). AWS and GCP are coming soon.",
"Secret vault not yet configured": "Secret vault not yet configured",
"OpenBao is not wired up to the Manager yet (see System settings → Secret vault status). The binding will be created in pending state — the client_secret will NOT be persisted server-side until vault is configured. Re-enter or rotate the secret once vault is online.": "OpenBao is not wired up to the Manager yet (see System settings → Secret vault status). The binding will be created in pending state — the client_secret will NOT be persisted server-side until vault is configured. Re-enter or rotate the secret once vault is online.",
"Azure Key Vault is not ready yet (see System settings → Secret vault status). The subscription cannot be bound until Manager can write the client_secret to Key Vault.": "Azure Key Vault is not ready yet (see System settings → Secret vault status). The subscription cannot be bound until Manager can write the client_secret to Key Vault.",
"Display name": "Display name",
"Subscription ID": "Subscription ID",
"Tenant ID": "Tenant ID",
@@ -4153,15 +4156,17 @@
"Client secret": "Client secret",
"What Heicode will and will not do": "What Heicode will and will not do",
"Read-only ARM access (Reader role recommended)": "Read-only ARM access (Reader role recommended)",
"Auto-discover VM / database / AKS / storage — coming soon": "Auto-discover VM / database / AKS / storage — coming soon",
"Auto-discover VM / database / AKS / storage": "Auto-discover VM / database / AKS / storage",
"Never modify resources without explicit approval from the desktop client": "Never modify resources without explicit approval from the desktop client",
"Never write the client_secret into Manager logs or audit payloads": "Never write the client_secret into Manager logs or audit payloads",
"Binding...": "Binding...",
"Bind subscription": "Bind subscription",
"Azure subscription bound": "Azure subscription bound",
"Azure discovery failed": "Azure discovery failed",
"{{n}} Azure resources discovered": "{{n}} Azure resources discovered",
"Failed to bind Azure account": "Failed to bind Azure account",
"Connect Azure": "Connect Azure",
"Authorize Azure (AWS / GCP coming soon). Resource auto-discovery later.": "Authorize Azure (AWS / GCP coming soon). Resource auto-discovery later.",
"Resource auto-discovery — coming soon": "Resource auto-discovery — coming soon"
"Authorize Azure (AWS / GCP coming soon). Heicode auto-discovers cloud resources.": "Authorize Azure (AWS / GCP coming soon). Heicode auto-discovers cloud resources.",
"{{n}} cloud account connected": "{{n}} cloud account connected"
}
}
+13 -8
View File
@@ -3949,6 +3949,7 @@
"WeChat login QR code": "微信登录二维码",
"WeChat QR code will be displayed here": "微信二维码将显示在此处",
"WeChat sign in": "微信登录",
"We recommend using a mainland China email address so verification emails arrive more reliably.": "建议使用国内邮箱注册,验证码邮件到达会更稳定。",
"Week": "周",
"Weekday": "星期",
"Weekly": "每周",
@@ -4120,8 +4121,8 @@
"{{n}} SK source connected": "已绑定 {{n}} 个 SK 来源",
"Pick a reusable skill / agent toolset repository, or skip.": "选一个可复用的技能包 / Agent 工具集仓库,也可以跳过。",
"Secret vault status": "密钥保管器状态",
"OpenBao / Vault health and seal state": "OpenBao / Vault 的健康状态和封存状态",
"Heicode stores long-lived credentials in OpenBao / Vault. This panel surfaces health and seal state for the operator — secret names and values are NEVER displayed here.": "Heicode 把长期凭证存在 OpenBao / Vault 里。这个面板只给运维同事看健康状态、是否封存——**永远不会**在这里显示密钥名称或明文。",
"Azure Key Vault reachability and managed identity state": "Azure Key Vault 可达性与托管身份状态",
"Heicode stores long-lived credentials in Azure Key Vault. This panel surfaces provider, managed identity and reachability status — secret names and values are NEVER displayed here.": "Heicode 把长期凭证存在 Azure Key Vault 里。这个面板只展示提供方、托管身份和可达性状态——永远不会显示密钥名称或明文。",
"Not configured": "未配置",
"Unreachable": "不可达",
"Sealed": "已封存",
@@ -4131,21 +4132,23 @@
"Failed to read secret store status": "读取密钥保管器状态失败",
"Refresh": "刷新",
"Configured": "已配置",
"Provider": "提供方",
"Auth method": "认证方式",
"Reachable": "可达",
"Initialized": "已初始化",
"Standby": "备用模式",
"Version": "版本",
"Last checked": "最近检查",
"To enable: set OPENBAO_ADDR + OPENBAO_TOKEN (or VAULT_*) env vars on the Manager container and restart. The status panel will pick up the connection on the next refresh.": "启用方法:在 Manager 容器上配置 OPENBAO_ADDR + OPENBAO_TOKEN 环境变量(或 VAULT_*),然后重启容器。状态面板会在下次刷新时自动识别。",
"To enable: set AZURE_KEY_VAULT_URL on the Manager container, enable the VM managed identity, grant it Key Vault secret permissions, and restart. The status panel will pick up the connection on the next refresh.": "启用方法:在 Manager 容器上配置 AZURE_KEY_VAULT_URL,启用 VM 托管身份,授予 Key Vault 密钥权限,然后重启容器。状态面板会在下次刷新时自动识别。",
"You do not have permission to view this": "你没有查看此内容的权限",
"Your account or token lacks the required permission. Contact an administrator to request access.": "你的账户或令牌缺少所需的权限。请联系管理员申请。",
"Network unreachable": "网络无法连接",
"Could not reach the server. Check your internet connection and the Heicode Manager status, then retry.": "无法连接服务器。请检查你的网络连接和 Heicode Manager 是否正常,然后重试。",
"Failed to load": "加载失败",
"Connect Azure subscription": "绑定 Azure 订阅",
"Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage) once auto-discovery lands. AWS and GCP are coming soon.": "粘贴一个对该订阅至少拥有 Reader 角色的 Service Principal。等资源自动发现功能上线后,Heicode 会用它枚举云资源(虚拟机、数据库、AKS、存储)。AWS 和 GCP 即将支持。",
"Paste a Service Principal that has at least Reader role on the subscription. Heicode uses it to enumerate cloud resources (VMs, databases, AKS, storage). AWS and GCP are coming soon.": "粘贴一个对该订阅至少拥有 Reader 角色的 Service Principal。Heicode 会用它枚举云资源(虚拟机、数据库、AKS、存储)。AWS 和 GCP 即将支持。",
"Secret vault not yet configured": "密钥保管器尚未配置",
"OpenBao is not wired up to the Manager yet (see System settings → Secret vault status). The binding will be created in pending state — the client_secret will NOT be persisted server-side until vault is configured. Re-enter or rotate the secret once vault is online.": "Manager 还没接上 OpenBao(详见 系统设置 → 密钥保管器状态)。绑定会以 pending 状态创建——在密钥保管器接通之前,client_secret 不会持久化到服务端。等保管器上线后请重新输入或轮换密钥。",
"Azure Key Vault is not ready yet (see System settings → Secret vault status). The subscription cannot be bound until Manager can write the client_secret to Key Vault.": "Azure Key Vault 尚未就绪(详见 系统设置 → 密钥保管器状态)。Manager 能把 client_secret 写入 Key Vault 之前,不能绑定订阅。",
"Display name": "显示名称",
"Subscription ID": "订阅 ID",
"Tenant ID": "租户 ID",
@@ -4153,15 +4156,17 @@
"Client secret": "Client secret",
"What Heicode will and will not do": "Heicode 会做什么 / 不会做什么",
"Read-only ARM access (Reader role recommended)": "只读 ARM 访问(推荐 Reader 角色)",
"Auto-discover VM / database / AKS / storage — coming soon": "自动发现 VM / 数据库 / AKS / 存储 — 即将上线",
"Auto-discover VM / database / AKS / storage": "自动发现 VM / 数据库 / AKS / 存储",
"Never modify resources without explicit approval from the desktop client": "未经桌面客户端明确审批,不会修改任何资源",
"Never write the client_secret into Manager logs or audit payloads": "不会把 client_secret 写入 Manager 日志或审计记录",
"Binding...": "绑定中...",
"Bind subscription": "绑定订阅",
"Azure subscription bound": "Azure 订阅已绑定",
"Azure discovery failed": "Azure 资源发现失败",
"{{n}} Azure resources discovered": "已发现 {{n}} 个 Azure 资源",
"Failed to bind Azure account": "Azure 账户绑定失败",
"Connect Azure": "绑定 Azure",
"Authorize Azure (AWS / GCP coming soon). Resource auto-discovery later.": "授权 Azure(AWS / GCP 即将支持)。资源自动发现稍后上线。",
"Resource auto-discovery — coming soon": "资源自动发现 — 即将上线"
"Authorize Azure (AWS / GCP coming soon). Heicode auto-discovers cloud resources.": "授权 Azure(AWS / GCP 即将支持)。Heicode 会自动发现云资源。",
"{{n}} cloud account connected": "已连接 {{n}} 个云账号"
}
}
+117
View File
@@ -0,0 +1,117 @@
import { api } from '@/lib/api'
export type ManagerResourceType =
| 'git'
| 'sk'
| 'project_document'
| 'cloud_account'
| 'cloud_resource'
export type ManagerResourceStatus =
| 'pending'
| 'active'
| 'disabled'
| 'revoked'
export type ManagerResource = {
id: number
user_id: number
tenant_id: string
project_id: string
binding_scope: string
name: string
resource_type: ManagerResourceType
provider: string
external_id: string
secret_ref: string
metadata: Record<string, unknown>
permission_scope: Record<string, unknown>
constraints: Record<string, unknown>
status: ManagerResourceStatus
created_at: number
updated_at: number
}
export type ManagerResourcePayload = {
tenant_id?: string
project_id?: string
binding_scope?: string
name: string
resource_type: ManagerResourceType
provider?: string
external_id?: string
secret_ref?: string
metadata?: Record<string, unknown>
permission_scope?: Record<string, unknown>
constraints?: Record<string, unknown>
status?: ManagerResourceStatus
}
type Envelope<T> = {
success: boolean
message?: string
data?: T
}
function unwrap<T>(env: Envelope<T>, fallback: string): T {
if (!env.success || env.data == null) {
throw new Error(env.message || fallback)
}
return env.data
}
export async function listManagerResources(params?: {
resource_type?: ManagerResourceType
status?: ManagerResourceStatus
}): Promise<ManagerResource[]> {
const res = await api.get<Envelope<{ items: ManagerResource[] }>>(
'/api/resources/',
{ params }
)
return res.data?.data?.items ?? []
}
export async function createManagerResource(
body: ManagerResourcePayload
): Promise<ManagerResource> {
const res = await api.post<Envelope<ManagerResource>>('/api/resources/', body)
return unwrap(res.data, 'createManagerResource failed')
}
export async function updateManagerResource(
id: number,
body: ManagerResourcePayload
): Promise<ManagerResource> {
const res = await api.put<Envelope<ManagerResource>>(
`/api/resources/${id}`,
body
)
return unwrap(res.data, 'updateManagerResource failed')
}
export async function putManagerResourceSecret(
id: number,
data: Record<string, unknown>
): Promise<{ resource: ManagerResource; secret_ref: string }> {
const res = await api.post<
Envelope<{ resource: ManagerResource; secret_ref: string }>
>(`/api/resources/${id}/secret`, { data })
return unwrap(res.data, 'putManagerResourceSecret failed')
}
export async function discoverManagerAzureResources(
id: number
): Promise<{
account: ManagerResource
items: ManagerResource[]
discovered: number
}> {
const res = await api.post<
Envelope<{
account: ManagerResource
items: ManagerResource[]
discovered: number
}>
>(`/api/resources/${id}/azure/discover`)
return unwrap(res.data, 'discoverManagerAzureResources failed')
}
+22
View File
@@ -45,6 +45,7 @@ import { Route as AuthenticatedPlaygroundIndexRouteImport } from './routes/_auth
import { Route as AuthenticatedModelsIndexRouteImport } from './routes/_authenticated/models/index'
import { Route as AuthenticatedKeysIndexRouteImport } from './routes/_authenticated/keys/index'
import { Route as AuthenticatedEventsIndexRouteImport } from './routes/_authenticated/events/index'
import { Route as AuthenticatedDevicesIndexRouteImport } from './routes/_authenticated/devices/index'
import { Route as AuthenticatedDesktopClientIndexRouteImport } from './routes/_authenticated/desktop-client/index'
import { Route as AuthenticatedDeploymentsIndexRouteImport } from './routes/_authenticated/deployments/index'
import { Route as AuthenticatedDashboardIndexRouteImport } from './routes/_authenticated/dashboard/index'
@@ -264,6 +265,12 @@ const AuthenticatedEventsIndexRoute =
path: '/events/',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
const AuthenticatedDevicesIndexRoute =
AuthenticatedDevicesIndexRouteImport.update({
id: '/devices/',
path: '/devices/',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
const AuthenticatedDesktopClientIndexRoute =
AuthenticatedDesktopClientIndexRouteImport.update({
id: '/desktop-client/',
@@ -464,6 +471,7 @@ export interface FileRoutesByFullPath {
'/dashboard/': typeof AuthenticatedDashboardIndexRoute
'/deployments/': typeof AuthenticatedDeploymentsIndexRoute
'/desktop-client/': typeof AuthenticatedDesktopClientIndexRoute
'/devices/': typeof AuthenticatedDevicesIndexRoute
'/events/': typeof AuthenticatedEventsIndexRoute
'/keys/': typeof AuthenticatedKeysIndexRoute
'/models/': typeof AuthenticatedModelsIndexRoute
@@ -527,6 +535,7 @@ export interface FileRoutesByTo {
'/dashboard': typeof AuthenticatedDashboardIndexRoute
'/deployments': typeof AuthenticatedDeploymentsIndexRoute
'/desktop-client': typeof AuthenticatedDesktopClientIndexRoute
'/devices': typeof AuthenticatedDevicesIndexRoute
'/events': typeof AuthenticatedEventsIndexRoute
'/keys': typeof AuthenticatedKeysIndexRoute
'/models': typeof AuthenticatedModelsIndexRoute
@@ -594,6 +603,7 @@ export interface FileRoutesById {
'/_authenticated/dashboard/': typeof AuthenticatedDashboardIndexRoute
'/_authenticated/deployments/': typeof AuthenticatedDeploymentsIndexRoute
'/_authenticated/desktop-client/': typeof AuthenticatedDesktopClientIndexRoute
'/_authenticated/devices/': typeof AuthenticatedDevicesIndexRoute
'/_authenticated/events/': typeof AuthenticatedEventsIndexRoute
'/_authenticated/keys/': typeof AuthenticatedKeysIndexRoute
'/_authenticated/models/': typeof AuthenticatedModelsIndexRoute
@@ -660,6 +670,7 @@ export interface FileRouteTypes {
| '/dashboard/'
| '/deployments/'
| '/desktop-client/'
| '/devices/'
| '/events/'
| '/keys/'
| '/models/'
@@ -723,6 +734,7 @@ export interface FileRouteTypes {
| '/dashboard'
| '/deployments'
| '/desktop-client'
| '/devices'
| '/events'
| '/keys'
| '/models'
@@ -789,6 +801,7 @@ export interface FileRouteTypes {
| '/_authenticated/dashboard/'
| '/_authenticated/deployments/'
| '/_authenticated/desktop-client/'
| '/_authenticated/devices/'
| '/_authenticated/events/'
| '/_authenticated/keys/'
| '/_authenticated/models/'
@@ -1091,6 +1104,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedEventsIndexRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
'/_authenticated/devices/': {
id: '/_authenticated/devices/'
path: '/devices'
fullPath: '/devices/'
preLoaderRoute: typeof AuthenticatedDevicesIndexRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
'/_authenticated/desktop-client/': {
id: '/_authenticated/desktop-client/'
path: '/desktop-client'
@@ -1387,6 +1407,7 @@ interface AuthenticatedRouteRouteChildren {
AuthenticatedDashboardIndexRoute: typeof AuthenticatedDashboardIndexRoute
AuthenticatedDeploymentsIndexRoute: typeof AuthenticatedDeploymentsIndexRoute
AuthenticatedDesktopClientIndexRoute: typeof AuthenticatedDesktopClientIndexRoute
AuthenticatedDevicesIndexRoute: typeof AuthenticatedDevicesIndexRoute
AuthenticatedEventsIndexRoute: typeof AuthenticatedEventsIndexRoute
AuthenticatedKeysIndexRoute: typeof AuthenticatedKeysIndexRoute
AuthenticatedModelsIndexRoute: typeof AuthenticatedModelsIndexRoute
@@ -1419,6 +1440,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
AuthenticatedDashboardIndexRoute: AuthenticatedDashboardIndexRoute,
AuthenticatedDeploymentsIndexRoute: AuthenticatedDeploymentsIndexRoute,
AuthenticatedDesktopClientIndexRoute: AuthenticatedDesktopClientIndexRoute,
AuthenticatedDevicesIndexRoute: AuthenticatedDevicesIndexRoute,
AuthenticatedEventsIndexRoute: AuthenticatedEventsIndexRoute,
AuthenticatedKeysIndexRoute: AuthenticatedKeysIndexRoute,
AuthenticatedModelsIndexRoute: AuthenticatedModelsIndexRoute,
+5 -1
View File
@@ -33,11 +33,15 @@ if not PASS:
print("MGR_PASS not set", file=sys.stderr)
sys.exit(2)
DB_PASS = "Myadmin@123456."
DB_PASS = os.environ.get("MGR_DB_PASS", "")
DB_HOST = "heicode.postgres.database.azure.com"
DB_USER = "heicode"
DB_NAME = "heicode"
if not DB_PASS:
print("MGR_DB_PASS not set", file=sys.stderr)
sys.exit(2)
# new-api token keys are 48-char alphanum (no padding). Match that
# exactly so admin tools / dashboards parse it.
ALPHABET = string.ascii_letters + string.digits
+10 -3
View File
@@ -1,6 +1,11 @@
#!/usr/bin/env python3
"""Look up which user account to attach the swarm-program token to."""
import os, sys, paramiko
import os, shlex, sys, paramiko
db_pass = os.environ.get("MGR_DB_PASS", "")
if not db_pass:
print("MGR_DB_PASS not set", file=sys.stderr)
sys.exit(2)
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -19,9 +24,11 @@ LIMIT 10;
# Wrap via psql exec inside the heicode container.
cmd = (
'sudo docker exec heicode sh -c '
+ '"PGPASSWORD=\\"Myadmin@123456.\\" psql '
+ shlex.quote(
"PGPASSWORD=" + shlex.quote(db_pass) + " psql "
+ '-h heicode.postgres.database.azure.com -U heicode heicode '
+ "-c \\\"" + sql.replace("\n", " ").replace('"', '\\"\\"') + "\\\"\""
+ "-c " + shlex.quote(sql.replace("\n", " "))
)
)
_, o, e = c.exec_command(cmd, timeout=60, get_pty=True)
for line in iter(o.readline, ""):