Make Agnet runs submit resource-scoped work safely
Manager now exposes a Resource Grant manifest and the Agnet control-plane response carries runtime state, agent instances, and permission_manifest so frontend runs can submit bounded resource grants without plaintext credentials. Constraint: Manager remains the user console while NewAPI stays independent and OpenBao is referenced through secret_ref only. Rejected: platform-side high-risk approval | client approval is the product boundary; Agnet only validates approval evidence. Confidence: medium Scope-risk: moderate Directive: Do not mix child Agnet runtime model selection with NewAPI billing or expose OpenBao as a public route. Tested: git diff --check; jq empty locale JSON; go vet ./controller ./model ./router; go test -count=1 ./controller ./model ./router Not-tested: frontend typecheck/build because local node_modules tooling is absent and user requested builds happen on the VM. Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
日期:2026-05-04
|
日期:2026-05-04
|
||||||
|
|
||||||
本文修正 Manager、Agnet 平台、NewAPI 与 OpenBao 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台代理审批描述冲突,以本文为准。
|
本文修正 Manager、Agnet 平台、NewAPI 与 OpenBao 之间的运行时边界。若本文与旧文档中 `tenant`、`project` 或平台侧批准描述冲突,以本文为准。
|
||||||
|
|
||||||
## 一、用户输入在哪里
|
## 一、用户输入在哪里
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
|
|||||||
|
|
||||||
## 三、高危操作审批与 OpenBao 密钥注入
|
## 三、高危操作审批与 OpenBao 密钥注入
|
||||||
|
|
||||||
高危操作审批只在客户端完成。用户在客户端明确批准后,Manager/Agnet 平台才可以执行对应动作。
|
高危操作审批只在客户端完成。用户在客户端明确批准后,Manager/Agnet 平台才可以执行对应动作。Agnet 平台不是审批主体,不发起额外审批;它只校验 `approval_id`、审批主体、审批范围、TTL、`risk_level` 和策略是否匹配。
|
||||||
|
|
||||||
密钥处理边界如下:
|
密钥处理边界如下:
|
||||||
|
|
||||||
@@ -89,6 +89,13 @@ Manager 可以有本地 user cache,但 canonical user identity 应来自登录
|
|||||||
- OpenBao 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 OpenBao。
|
- OpenBao 不应作为普通公网路由开放;如果 Manager 已经提供客户端验证、资源绑定、审批和 `secret_ref` 管理接口,客户端不需要直连 OpenBao。
|
||||||
- OpenBao 访问应限制在容器网络、VM loopback、AKS 内网、Workload Identity 或其它受保护服务间通道。
|
- OpenBao 访问应限制在容器网络、VM loopback、AKS 内网、Workload Identity 或其它受保护服务间通道。
|
||||||
|
|
||||||
|
OpenBao 不暴露公网的检查口径:
|
||||||
|
|
||||||
|
- 公网 DNS、Nginx `server_name`、Ingress、LoadBalancer 和安全组规则不得直接指向 OpenBao 服务端口。
|
||||||
|
- 外部客户端只能通过 Manager 的认证、资源绑定、审批和 `secret_ref` 管理接口间接操作密钥引用。
|
||||||
|
- Manager、Agnet 平台和子 Agnet 访问 OpenBao 时必须走内网地址、loopback、容器网络、AKS private endpoint 或 Workload Identity 绑定。
|
||||||
|
- 健康检查和联调报告只能证明内网访问可用;不得把公网可访问的 OpenBao health endpoint 作为验收口径。
|
||||||
|
|
||||||
短期凭证注入必须满足:
|
短期凭证注入必须满足:
|
||||||
|
|
||||||
- 有客户端审批记录。
|
- 有客户端审批记录。
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ POST /api/agnet/deployments
|
|||||||
| `intent_id` | string | 是 | Manager 侧意图 ID,用于幂等、审计和追踪。 |
|
| `intent_id` | string | 是 | Manager 侧意图 ID,用于幂等、审计和追踪。 |
|
||||||
| `template_hint` | string | 是 | Agnet 平台选择编排模板的提示,如 `manager-resource-binding`。 |
|
| `template_hint` | string | 是 | Agnet 平台选择编排模板的提示,如 `manager-resource-binding`。 |
|
||||||
| `objective` | string | 是 | 本次部署目标,应是自然语言但不能含密钥。 |
|
| `objective` | string | 是 | 本次部署目标,应是自然语言但不能含密钥。 |
|
||||||
| `risk_level` | enum | 是 | `low` / `medium` / `high`。高风险应触发审批或只读模式。 |
|
| `risk_level` | enum | 是 | `low` / `medium` / `high`。高风险必须携带客户端审批证据;缺失或不匹配时只能只读或拒绝执行。 |
|
||||||
| `budget.max_tokens` | int | 是 | 当前策略上限建议不超过 `500000`。 |
|
| `budget.max_tokens` | int | 是 | 当前策略上限建议不超过 `500000`。 |
|
||||||
| `budget.max_cost_usd` | number | 是 | 当前策略上限建议不超过 `200`。 |
|
| `budget.max_cost_usd` | number | 是 | 当前策略上限建议不超过 `200`。 |
|
||||||
| `budget.max_duration_sec` | int | 是 | 当前策略上限建议不超过 `86400`。 |
|
| `budget.max_duration_sec` | int | 是 | 当前策略上限建议不超过 `86400`。 |
|
||||||
@@ -899,10 +899,23 @@ Manager 发给 Agnet 平台前必须执行:
|
|||||||
2. 凭据型资源只传 `secret_ref`,不传明文凭据。
|
2. 凭据型资源只传 `secret_ref`,不传明文凭据。
|
||||||
3. `metadata`、`constraints`、`audit` 的 key 中不得出现 `password`、`token`、`secret`、`private_key`、`access_key`、`credential` 等敏感词。
|
3. `metadata`、`constraints`、`audit` 的 key 中不得出现 `password`、`token`、`secret`、`private_key`、`access_key`、`credential` 等敏感词。
|
||||||
4. `repo_url` 不得包含用户名、密码或访问 Token。
|
4. `repo_url` 不得包含用户名、密码或访问 Token。
|
||||||
5. `permission_scope` 使用最小权限,生产写操作需审批记录或平台代理执行。
|
5. `permission_scope` 使用最小权限;生产写操作必须携带客户端审批记录,Agnet 平台不得自行补批。
|
||||||
6. 高风险操作(生产部署、云资源修改、删除、扩容)应设置 `risk_level=high` 并由 Agnet 平台二次审批。
|
6. 高风险操作(生产部署、云资源修改、删除、扩容)必须设置 `risk_level=high`,Agnet 平台执行前只校验客户端审批证据。
|
||||||
7. 所有日志/事件/审计返回给 Manager 前必须脱敏。
|
7. 所有日志/事件/审计返回给 Manager 前必须脱敏。
|
||||||
|
|
||||||
|
Agnet 平台不承担高危操作审批主体。审批只发生在客户端;Agnet 平台只能在执行前校验以下字段和策略是否一致:
|
||||||
|
|
||||||
|
| 校验项 | 要求 |
|
||||||
|
|---|---|
|
||||||
|
| `approval_id` | 必须存在于高危任务的 `constraints` 或 `audit`,并可追溯到客户端审批记录。 |
|
||||||
|
| 审批主体 | 审批用户必须与 `user_context.user_id`、`resource_grants[].user_id` 或授权代理主体一致。 |
|
||||||
|
| 审批范围 | 审批范围必须覆盖 `binding_scope`、`permission_scope`、目标环境、资源 ID 和操作类型。 |
|
||||||
|
| TTL / 时间窗口 | 审批记录必须未过期;若使用 `window` 或 TTL,当前执行时间必须落在允许范围内。 |
|
||||||
|
| `risk_level` | 高危资源写入、生产部署、云资源修改、删除和扩容必须为 `high`。 |
|
||||||
|
| 策略 | 平台 policy、OpenBao policy、Kubernetes/Workload Identity、网络策略和最小权限约束均必须允许本次动作。 |
|
||||||
|
|
||||||
|
任一校验不通过时,Agnet 平台应返回 `POLICY_REJECTED` 或 `FORBIDDEN_SCOPE`,不得发起额外批准流程。
|
||||||
|
|
||||||
### 8.1 字段级约束速查
|
### 8.1 字段级约束速查
|
||||||
|
|
||||||
| 对象/接口 | 必填最小集合 | 禁止内容 |
|
| 对象/接口 | 必填最小集合 | 禁止内容 |
|
||||||
|
|||||||
@@ -148,12 +148,43 @@ type agnetDeploymentRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type agnetDeploymentRecord struct {
|
type agnetDeploymentRecord struct {
|
||||||
DeploymentID string `json:"deployment_id"`
|
DeploymentID string `json:"deployment_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Phase string `json:"phase"`
|
Phase string `json:"phase"`
|
||||||
CreatedAt string `json:"created_at"`
|
RuntimeState string `json:"runtime_state"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
FailureReason string `json:"failure_reason"`
|
||||||
Plan agnetOrchestrationPlan `json:"orchestration_plan"`
|
AgentInstances []agnetAgentInstance `json:"agent_instances"`
|
||||||
|
ResourceGrantManifest agnetPermissionManifest `json:"permission_manifest"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
Plan agnetOrchestrationPlan `json:"orchestration_plan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agnetAgentInstance struct {
|
||||||
|
InstanceID string `json:"instance_id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
RuntimeState string `json:"runtime_state"`
|
||||||
|
FailureReason string `json:"failure_reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agnetManifestGrant struct {
|
||||||
|
GrantID string `json:"grant_id"`
|
||||||
|
ResourceID string `json:"resource_id"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceRef string `json:"resource_ref"`
|
||||||
|
AllowedActions []string `json:"allowed_actions"`
|
||||||
|
Constraints map[string]string `json:"constraints"`
|
||||||
|
SecretRef string `json:"secret_ref,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agnetPermissionManifest struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
BindingScope string `json:"binding_scope"`
|
||||||
|
AgentRole string `json:"agent_role"`
|
||||||
|
TargetAgentRef string `json:"target_agent_ref"`
|
||||||
|
ResourceGrants []agnetManifestGrant `json:"resource_grants"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type agnetEvent struct {
|
type agnetEvent struct {
|
||||||
@@ -518,6 +549,80 @@ func firstPlanBindingScope(plan agnetOrchestrationPlan) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func agnetGrantResourceRef(grant agnetResourceGrant) string {
|
||||||
|
for _, value := range []string{grant.Metadata["repo_url"], grant.Metadata["doc_ref"], grant.Metadata["resource_ref"], grant.BindingScope, grant.ResourceID} {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grant.GrantID
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAgnetPermissionManifest(plan agnetOrchestrationPlan) agnetPermissionManifest {
|
||||||
|
manifest := agnetPermissionManifest{
|
||||||
|
UserID: plan.UserContext.UserID,
|
||||||
|
BindingScope: firstPlanBindingScope(plan),
|
||||||
|
}
|
||||||
|
for _, agent := range plan.Agents {
|
||||||
|
for _, grant := range agent.ResourceGrants {
|
||||||
|
if strings.TrimSpace(grant.Status) != agnetGrantStatusActive {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if manifest.AgentRole == "" {
|
||||||
|
manifest.AgentRole = grant.TargetRole
|
||||||
|
}
|
||||||
|
if manifest.TargetAgentRef == "" {
|
||||||
|
manifest.TargetAgentRef = grant.TargetAgentRef
|
||||||
|
}
|
||||||
|
manifest.ResourceGrants = append(manifest.ResourceGrants, agnetManifestGrant{
|
||||||
|
GrantID: grant.GrantID,
|
||||||
|
ResourceID: grant.ResourceID,
|
||||||
|
ResourceType: grant.ResourceType,
|
||||||
|
ResourceRef: agnetGrantResourceRef(grant),
|
||||||
|
AllowedActions: append([]string{}, grant.PermissionScope...),
|
||||||
|
Constraints: grant.Constraints,
|
||||||
|
SecretRef: grant.SecretRef,
|
||||||
|
Status: grant.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if manifest.ResourceGrants == nil {
|
||||||
|
manifest.ResourceGrants = []agnetManifestGrant{}
|
||||||
|
}
|
||||||
|
return manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAgnetAgentInstances(plan agnetOrchestrationPlan, phase string, runtimeState string) []agnetAgentInstance {
|
||||||
|
instances := make([]agnetAgentInstance, 0)
|
||||||
|
if len(plan.AgentRuntime.Agents) > 0 {
|
||||||
|
for _, runtimeAgent := range plan.AgentRuntime.Agents {
|
||||||
|
count := runtimeAgent.InstanceCount
|
||||||
|
if count <= 0 {
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
instances = append(instances, agnetAgentInstance{
|
||||||
|
InstanceID: "agi_" + common.GetUUID()[:12],
|
||||||
|
Role: runtimeAgent.Role,
|
||||||
|
Phase: phase,
|
||||||
|
RuntimeState: runtimeState,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(instances) == 0 {
|
||||||
|
for _, agent := range plan.Agents {
|
||||||
|
instances = append(instances, agnetAgentInstance{
|
||||||
|
InstanceID: "agi_" + common.GetUUID()[:12],
|
||||||
|
Role: agent.RoleTemplate,
|
||||||
|
Phase: phase,
|
||||||
|
RuntimeState: runtimeState,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instances
|
||||||
|
}
|
||||||
|
|
||||||
func planHasBindingScope(plan agnetOrchestrationPlan, bindingScope string) bool {
|
func planHasBindingScope(plan agnetOrchestrationPlan, bindingScope string) bool {
|
||||||
bindingScope = strings.TrimSpace(bindingScope)
|
bindingScope = strings.TrimSpace(bindingScope)
|
||||||
if bindingScope == "" {
|
if bindingScope == "" {
|
||||||
@@ -566,12 +671,16 @@ func AgnetCreateDeployment(c *gin.Context) {
|
|||||||
now := agnetNow()
|
now := agnetNow()
|
||||||
deploymentID := "dep_" + common.GetUUID()[:12]
|
deploymentID := "dep_" + common.GetUUID()[:12]
|
||||||
record := agnetDeploymentRecord{
|
record := agnetDeploymentRecord{
|
||||||
DeploymentID: deploymentID,
|
DeploymentID: deploymentID,
|
||||||
Status: "accepted",
|
Status: "accepted",
|
||||||
Phase: "pending",
|
Phase: "pending",
|
||||||
CreatedAt: now,
|
RuntimeState: "queued",
|
||||||
UpdatedAt: now,
|
FailureReason: "",
|
||||||
Plan: plan,
|
AgentInstances: buildAgnetAgentInstances(plan, "pending", "queued"),
|
||||||
|
ResourceGrantManifest: buildAgnetPermissionManifest(plan),
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
Plan: plan,
|
||||||
}
|
}
|
||||||
event := agnetEvent{
|
event := agnetEvent{
|
||||||
EventID: "evt_" + common.GetUUID()[:12],
|
EventID: "evt_" + common.GetUUID()[:12],
|
||||||
@@ -591,15 +700,13 @@ func AgnetCreateDeployment(c *gin.Context) {
|
|||||||
agnetMu.Unlock()
|
agnetMu.Unlock()
|
||||||
|
|
||||||
common.ApiSuccess(c, gin.H{
|
common.ApiSuccess(c, gin.H{
|
||||||
"deployment_id": deploymentID,
|
"deployment_id": deploymentID,
|
||||||
"status": "accepted",
|
"status": record.Status,
|
||||||
"agent_instances": []gin.H{
|
"phase": record.Phase,
|
||||||
{
|
"runtime_state": record.RuntimeState,
|
||||||
"instance_id": "agi_" + common.GetUUID()[:12],
|
"failure_reason": record.FailureReason,
|
||||||
"role": plan.Agents[0].RoleTemplate,
|
"agent_instances": record.AgentInstances,
|
||||||
"phase": "pending",
|
"permission_manifest": record.ResourceGrantManifest,
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,6 +765,13 @@ func AgnetStopDeployment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
record.Status = "stopped"
|
record.Status = "stopped"
|
||||||
record.Phase = "stopped"
|
record.Phase = "stopped"
|
||||||
|
record.RuntimeState = "stopped"
|
||||||
|
record.FailureReason = ""
|
||||||
|
for i := range record.AgentInstances {
|
||||||
|
record.AgentInstances[i].Phase = "stopped"
|
||||||
|
record.AgentInstances[i].RuntimeState = "stopped"
|
||||||
|
record.AgentInstances[i].FailureReason = ""
|
||||||
|
}
|
||||||
record.UpdatedAt = agnetNow()
|
record.UpdatedAt = agnetNow()
|
||||||
agnetDeployments[deploymentID] = record
|
agnetDeployments[deploymentID] = record
|
||||||
agnetEvents[deploymentID] = append(agnetEvents[deploymentID], agnetEvent{
|
agnetEvents[deploymentID] = append(agnetEvents[deploymentID], agnetEvent{
|
||||||
@@ -676,6 +790,8 @@ func AgnetStopDeployment(c *gin.Context) {
|
|||||||
common.ApiSuccess(c, gin.H{
|
common.ApiSuccess(c, gin.H{
|
||||||
"deployment_id": deploymentID,
|
"deployment_id": deploymentID,
|
||||||
"status": "stopped",
|
"status": "stopped",
|
||||||
|
"phase": record.Phase,
|
||||||
|
"runtime_state": record.RuntimeState,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,6 +834,8 @@ func AgnetListDeploymentLogs(c *gin.Context) {
|
|||||||
"level": "info",
|
"level": "info",
|
||||||
"message": "deployment accepted by Manager control-plane placeholder",
|
"message": "deployment accepted by Manager control-plane placeholder",
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
|
"runtime_state": record.RuntimeState,
|
||||||
|
"failure_reason": record.FailureReason,
|
||||||
"correlation_id": record.Plan.Metadata.CorrelationID,
|
"correlation_id": record.Plan.Metadata.CorrelationID,
|
||||||
"redacted": true,
|
"redacted": true,
|
||||||
})
|
})
|
||||||
@@ -729,6 +847,8 @@ func AgnetListDeploymentLogs(c *gin.Context) {
|
|||||||
"level": "info",
|
"level": "info",
|
||||||
"message": event.Event,
|
"message": event.Event,
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
|
"runtime_state": record.RuntimeState,
|
||||||
|
"failure_reason": record.FailureReason,
|
||||||
"correlation_id": event.CorrelationID,
|
"correlation_id": event.CorrelationID,
|
||||||
"redacted": true,
|
"redacted": true,
|
||||||
})
|
})
|
||||||
@@ -759,11 +879,13 @@ func AgnetGetDeploymentMetrics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
common.ApiSuccess(c, gin.H{
|
common.ApiSuccess(c, gin.H{
|
||||||
"deployment_id": deploymentID,
|
"deployment_id": deploymentID,
|
||||||
"window": strings.TrimSpace(c.DefaultQuery("window", "15m")),
|
"window": strings.TrimSpace(c.DefaultQuery("window", "15m")),
|
||||||
"step": strings.TrimSpace(c.DefaultQuery("step", "60s")),
|
"step": strings.TrimSpace(c.DefaultQuery("step", "60s")),
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
"status": record.Status,
|
"status": record.Status,
|
||||||
|
"runtime_state": record.RuntimeState,
|
||||||
|
"failure_reason": record.FailureReason,
|
||||||
"resource_usage": gin.H{
|
"resource_usage": gin.H{
|
||||||
"cpu_percent": 0,
|
"cpu_percent": 0,
|
||||||
"memory_bytes": 0,
|
"memory_bytes": 0,
|
||||||
|
|||||||
@@ -166,6 +166,34 @@ func TestAgnetCreateDeploymentAcceptsP1ResourceGrantModel(t *testing.T) {
|
|||||||
require.Equal(t, "channel-p1", stored.Plan.UserContext.ChannelID)
|
require.Equal(t, "channel-p1", stored.Plan.UserContext.ChannelID)
|
||||||
require.Equal(t, "agnet", stored.Plan.AgentRuntime.Platform)
|
require.Equal(t, "agnet", stored.Plan.AgentRuntime.Platform)
|
||||||
require.Equal(t, "agnet-model-profile-builder", stored.Plan.AgentRuntime.Agents[0].ModelRef)
|
require.Equal(t, "agnet-model-profile-builder", stored.Plan.AgentRuntime.Agents[0].ModelRef)
|
||||||
|
require.Equal(t, "queued", stored.RuntimeState)
|
||||||
|
require.Empty(t, stored.FailureReason)
|
||||||
|
require.Len(t, stored.AgentInstances, 1)
|
||||||
|
require.Equal(t, "queued", stored.AgentInstances[0].RuntimeState)
|
||||||
|
require.Len(t, stored.ResourceGrantManifest.ResourceGrants, 2)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetPermissionManifestExcludesInactiveResourceGrants(t *testing.T) {
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.Agents[0].ResourceGrants[1].Status = agnetGrantStatusRevoked
|
||||||
|
|
||||||
|
_, envelope := postAgnetCreateDeployment(t, plan)
|
||||||
|
require.True(t, envelope.Success)
|
||||||
|
|
||||||
|
agnetMu.RLock()
|
||||||
|
var stored agnetDeploymentRecord
|
||||||
|
for _, record := range agnetDeployments {
|
||||||
|
stored = record
|
||||||
|
}
|
||||||
|
agnetMu.RUnlock()
|
||||||
|
|
||||||
|
require.Len(t, stored.ResourceGrantManifest.ResourceGrants, 1)
|
||||||
|
require.Equal(t, "grant-git-builder", stored.ResourceGrantManifest.ResourceGrants[0].GrantID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAgnetCreateDeploymentRejectsPlaintextResourceGrantCredentialFields(t *testing.T) {
|
func TestAgnetCreateDeploymentRejectsPlaintextResourceGrantCredentialFields(t *testing.T) {
|
||||||
@@ -292,4 +320,6 @@ func TestAgnetDeploymentLogsAndMetricsExposeRedactedReadiness(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusOK, metricRecorder.Code)
|
require.Equal(t, http.StatusOK, metricRecorder.Code)
|
||||||
require.Contains(t, metricRecorder.Body.String(), `"deployment_id":"`+deploymentID+`"`)
|
require.Contains(t, metricRecorder.Body.String(), `"deployment_id":"`+deploymentID+`"`)
|
||||||
require.Contains(t, metricRecorder.Body.String(), `"platform_estimated":true`)
|
require.Contains(t, metricRecorder.Body.String(), `"platform_estimated":true`)
|
||||||
|
require.Contains(t, metricRecorder.Body.String(), `"runtime_state":"queued"`)
|
||||||
|
require.Contains(t, metricRecorder.Body.String(), `"failure_reason":""`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,26 @@ type resourceGrantResponse struct {
|
|||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resourceGrantManifestGrant struct {
|
||||||
|
GrantId int `json:"grant_id"`
|
||||||
|
ResourceId int `json:"resource_id"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceRef string `json:"resource_ref"`
|
||||||
|
AllowedActions []any `json:"allowed_actions"`
|
||||||
|
PermissionScope map[string]any `json:"permission_scope"`
|
||||||
|
Constraints map[string]any `json:"constraints"`
|
||||||
|
SecretRef string `json:"secret_ref,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceGrantManifestResponse struct {
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
BindingScope string `json:"binding_scope"`
|
||||||
|
AgentRole string `json:"agent_role"`
|
||||||
|
TargetAgentRef string `json:"target_agent_ref"`
|
||||||
|
ResourceGrants []resourceGrantManifestGrant `json:"resource_grants"`
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeResourcePayload(p resourcePayload) (resourcePayload, error) {
|
func normalizeResourcePayload(p resourcePayload) (resourcePayload, error) {
|
||||||
p.TenantId = strings.TrimSpace(p.TenantId)
|
p.TenantId = strings.TrimSpace(p.TenantId)
|
||||||
p.ProjectId = strings.TrimSpace(p.ProjectId)
|
p.ProjectId = strings.TrimSpace(p.ProjectId)
|
||||||
@@ -314,6 +334,54 @@ func resourceGrantToResponse(grant model.ResourceGrant, resource *model.Resource
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceGrantToManifestGrant(grant model.ResourceGrant, resource model.ResourceBinding) resourceGrantManifestGrant {
|
||||||
|
permissionScope := unmarshalResourceJSON(grant.PermissionScope)
|
||||||
|
return resourceGrantManifestGrant{
|
||||||
|
GrantId: grant.Id,
|
||||||
|
ResourceId: grant.ResourceId,
|
||||||
|
ResourceType: resource.ResourceType,
|
||||||
|
ResourceRef: resourceManifestRef(resource),
|
||||||
|
AllowedActions: manifestAllowedActions(permissionScope),
|
||||||
|
PermissionScope: permissionScope,
|
||||||
|
Constraints: unmarshalResourceJSON(grant.Constraints),
|
||||||
|
SecretRef: resource.SecretRef,
|
||||||
|
Status: grant.Status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceManifestRef(resource model.ResourceBinding) string {
|
||||||
|
for _, value := range []string{resource.ExternalId, resource.BindingScope, resource.Name} {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("resource:%d", resource.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func manifestAllowedActions(permissionScope map[string]any) []any {
|
||||||
|
actions, ok := permissionScope["actions"]
|
||||||
|
if !ok {
|
||||||
|
return []any{}
|
||||||
|
}
|
||||||
|
switch v := actions.(type) {
|
||||||
|
case []any:
|
||||||
|
return v
|
||||||
|
case []string:
|
||||||
|
out := make([]any, 0, len(v))
|
||||||
|
for _, action := range v {
|
||||||
|
out = append(out, action)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case string:
|
||||||
|
if strings.TrimSpace(v) == "" {
|
||||||
|
return []any{}
|
||||||
|
}
|
||||||
|
return []any{v}
|
||||||
|
default:
|
||||||
|
return []any{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ListResources(c *gin.Context) {
|
func ListResources(c *gin.Context) {
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
query := model.DB.Where("user_id = ?", userId)
|
query := model.DB.Where("user_id = ?", userId)
|
||||||
@@ -564,6 +632,51 @@ func ListResourceGrants(c *gin.Context) {
|
|||||||
common.ApiSuccess(c, gin.H{"items": items})
|
common.ApiSuccess(c, gin.H{"items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateResourceGrantManifest(c *gin.Context) {
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
bindingScope := strings.TrimSpace(c.Query("binding_scope"))
|
||||||
|
agentId := strings.TrimSpace(c.Query("agnet_id"))
|
||||||
|
role := strings.TrimSpace(c.Query("role"))
|
||||||
|
|
||||||
|
query := model.DB.Where("user_id = ? AND status = ?", userId, "active")
|
||||||
|
if bindingScope != "" {
|
||||||
|
query = query.Where("binding_scope = ?", bindingScope)
|
||||||
|
}
|
||||||
|
if agentId != "" {
|
||||||
|
query = query.Where("agnet_id = ?", agentId)
|
||||||
|
}
|
||||||
|
if role != "" {
|
||||||
|
query = query.Where("role = ?", role)
|
||||||
|
}
|
||||||
|
|
||||||
|
var grants []model.ResourceGrant
|
||||||
|
if err := query.Order("id asc").Find(&grants).Error; err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]resourceGrantManifestGrant, 0, len(grants))
|
||||||
|
for _, grant := range grants {
|
||||||
|
var resource model.ResourceBinding
|
||||||
|
if err := model.DB.Where("id = ? AND user_id = ? AND status = ?", grant.ResourceId, userId, "active").First(&resource).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items = append(items, resourceGrantToManifestGrant(grant, resource))
|
||||||
|
}
|
||||||
|
|
||||||
|
common.ApiSuccess(c, resourceGrantManifestResponse{
|
||||||
|
UserId: userId,
|
||||||
|
BindingScope: bindingScope,
|
||||||
|
AgentRole: role,
|
||||||
|
TargetAgentRef: agentId,
|
||||||
|
ResourceGrants: items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func CreateResourceGrant(c *gin.Context) {
|
func CreateResourceGrant(c *gin.Context) {
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
var payload resourceGrantPayload
|
var payload resourceGrantPayload
|
||||||
|
|||||||
@@ -164,6 +164,61 @@ func TestCreateResourceGrantRejectsMismatchedBindingScope(t *testing.T) {
|
|||||||
require.Contains(t, w.Body.String(), "resource binding_scope does not match grant binding_scope")
|
require.Contains(t, w.Body.String(), "resource binding_scope does not match grant binding_scope")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
|
||||||
|
db := setupResourceControllerTestDB(t)
|
||||||
|
resource := model.ResourceBinding{
|
||||||
|
UserId: 7,
|
||||||
|
BindingScope: "repo-main",
|
||||||
|
Name: "Project repository",
|
||||||
|
ResourceType: "git",
|
||||||
|
Provider: "github",
|
||||||
|
ExternalId: "https://example.com/org/repo.git",
|
||||||
|
SecretRef: "vault://secret/resources/repo-main",
|
||||||
|
Metadata: `{"repo_url":"https://example.com/org/repo.git"}`,
|
||||||
|
PermissionScope: `{"actions":["read","write"]}`,
|
||||||
|
Status: "active",
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Create(&resource).Error)
|
||||||
|
require.NoError(t, db.Create(&model.ResourceGrant{
|
||||||
|
UserId: 7,
|
||||||
|
BindingScope: "repo-main",
|
||||||
|
ResourceId: resource.Id,
|
||||||
|
Role: "backend",
|
||||||
|
AgnetId: "agnet-backend-1",
|
||||||
|
PermissionScope: `{"actions":["read"]}`,
|
||||||
|
Constraints: `{"paths":["heicode/controller/**"]}`,
|
||||||
|
Status: "active",
|
||||||
|
}).Error)
|
||||||
|
require.NoError(t, db.Create(&model.ResourceGrant{
|
||||||
|
UserId: 7,
|
||||||
|
BindingScope: "repo-main",
|
||||||
|
ResourceId: resource.Id,
|
||||||
|
Role: "backend",
|
||||||
|
AgnetId: "agnet-backend-1",
|
||||||
|
PermissionScope: `{"actions":["write"]}`,
|
||||||
|
Status: "revoked",
|
||||||
|
}).Error)
|
||||||
|
|
||||||
|
w := performResourceRequestWithRoute(
|
||||||
|
GenerateResourceGrantManifest,
|
||||||
|
7,
|
||||||
|
http.MethodGet,
|
||||||
|
"/manifest",
|
||||||
|
"/manifest?binding_scope=repo-main&role=backend&agnet_id=agnet-backend-1",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code)
|
||||||
|
require.Contains(t, w.Body.String(), `"success":true`)
|
||||||
|
require.Contains(t, w.Body.String(), `"binding_scope":"repo-main"`)
|
||||||
|
require.Contains(t, w.Body.String(), `"agent_role":"backend"`)
|
||||||
|
require.Contains(t, w.Body.String(), `"target_agent_ref":"agnet-backend-1"`)
|
||||||
|
require.Contains(t, w.Body.String(), `"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.NotContains(t, w.Body.String(), `"allowed_actions":["write"]`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) {
|
func TestUpsertResourceSecretWritesOpenBaoAndStoresOnlySecretRef(t *testing.T) {
|
||||||
db := setupResourceControllerTestDB(t)
|
db := setupResourceControllerTestDB(t)
|
||||||
resource := model.ResourceBinding{
|
resource := model.ResourceBinding{
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
resourceGrantRoute.Use(middleware.UserAuth())
|
resourceGrantRoute.Use(middleware.UserAuth())
|
||||||
{
|
{
|
||||||
resourceGrantRoute.GET("/", controller.ListResourceGrants)
|
resourceGrantRoute.GET("/", controller.ListResourceGrants)
|
||||||
|
resourceGrantRoute.GET("/manifest", controller.GenerateResourceGrantManifest)
|
||||||
resourceGrantRoute.POST("/", controller.CreateResourceGrant)
|
resourceGrantRoute.POST("/", controller.CreateResourceGrant)
|
||||||
resourceGrantRoute.PUT("/:id", controller.UpdateResourceGrant)
|
resourceGrantRoute.PUT("/:id", controller.UpdateResourceGrant)
|
||||||
resourceGrantRoute.DELETE("/:id", controller.DeleteResourceGrant)
|
resourceGrantRoute.DELETE("/:id", controller.DeleteResourceGrant)
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export type AgnetAgentPlan = {
|
|||||||
binding_scope?: string
|
binding_scope?: string
|
||||||
target_role?: string
|
target_role?: string
|
||||||
target_agent_ref?: string
|
target_agent_ref?: string
|
||||||
|
permission_scope?: string[]
|
||||||
|
constraints?: Record<string, string>
|
||||||
|
metadata?: Record<string, string>
|
||||||
|
status?: 'pending' | 'active' | 'disabled' | 'revoked'
|
||||||
|
secret_ref?: string
|
||||||
|
audit?: Record<string, string>
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+920
-386
File diff suppressed because it is too large
Load Diff
+230
-86
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState, type ComponentType, type ReactNode } from 'react'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
@@ -78,7 +78,7 @@ function classifyStatus(status: string): StatusKey {
|
|||||||
|
|
||||||
function StatusBadge({ phase }: { phase: string }) {
|
function StatusBadge({ phase }: { phase: string }) {
|
||||||
const k = classifyStatus(phase)
|
const k = classifyStatus(phase)
|
||||||
const map: Record<StatusKey, { cls: string; icon: React.ReactNode }> = {
|
const map: Record<StatusKey, { cls: string; icon: ReactNode }> = {
|
||||||
running: {
|
running: {
|
||||||
cls: 'bg-[color-mix(in_oklch,var(--primary)_22%,transparent)] text-primary ring-primary/40',
|
cls: 'bg-[color-mix(in_oklch,var(--primary)_22%,transparent)] text-primary ring-primary/40',
|
||||||
icon: <PlayCircle className='h-3 w-3' />,
|
icon: <PlayCircle className='h-3 w-3' />,
|
||||||
@@ -113,8 +113,8 @@ function StatusBadge({ phase }: { phase: string }) {
|
|||||||
function PageSurface(props: {
|
function PageSurface(props: {
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
toolbar?: React.ReactNode
|
toolbar?: ReactNode
|
||||||
children: React.ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className='space-y-5 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-5 backdrop-blur'>
|
<section className='space-y-5 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_70%,transparent)] p-5 backdrop-blur'>
|
||||||
@@ -174,7 +174,7 @@ function MetaPill({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<{ className?: string }>
|
icon: ComponentType<{ className?: string }>
|
||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
}) {
|
}) {
|
||||||
@@ -193,6 +193,12 @@ function describeRiskLevel(dep: AgnetDeployment): {
|
|||||||
label: string
|
label: string
|
||||||
tone: 'low' | 'mid' | 'high'
|
tone: 'low' | 'mid' | 'high'
|
||||||
} {
|
} {
|
||||||
|
const explicit = dep.orchestration_plan?.risk_level?.toLowerCase()
|
||||||
|
if (explicit === 'high') return { label: 'high', tone: 'high' }
|
||||||
|
if (explicit === 'medium' || explicit === 'mid')
|
||||||
|
return { label: 'medium', tone: 'mid' }
|
||||||
|
if (explicit === 'low') return { label: 'low', tone: 'low' }
|
||||||
|
|
||||||
const objective = (dep.orchestration_plan?.objective || '').toLowerCase()
|
const objective = (dep.orchestration_plan?.objective || '').toLowerCase()
|
||||||
if (objective.includes('production') || objective.includes('critical')) {
|
if (objective.includes('production') || objective.includes('critical')) {
|
||||||
return { label: 'high', tone: 'high' }
|
return { label: 'high', tone: 'high' }
|
||||||
@@ -204,9 +210,18 @@ function describeRiskLevel(dep: AgnetDeployment): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function describeBudget(dep: AgnetDeployment): string {
|
function describeBudget(dep: AgnetDeployment): string {
|
||||||
const plan = dep.orchestration_plan
|
const budget = dep.orchestration_plan?.budget
|
||||||
const agents = plan?.agents?.length ?? 0
|
if (!budget) {
|
||||||
return `${agents} agents`
|
const agents = dep.orchestration_plan?.agents?.length ?? 0
|
||||||
|
return `${agents} agents`
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = []
|
||||||
|
if (budget.max_cost_usd > 0) parts.push(`$${budget.max_cost_usd}`)
|
||||||
|
if (budget.max_tokens > 0) parts.push(`${budget.max_tokens} tokens`)
|
||||||
|
if (budget.max_duration_sec > 0)
|
||||||
|
parts.push(`${Math.round(budget.max_duration_sec / 60)}m`)
|
||||||
|
return parts.length > 0 ? parts.join(' / ') : '—'
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeScope(dep: AgnetDeployment): string {
|
function describeScope(dep: AgnetDeployment): string {
|
||||||
@@ -220,6 +235,25 @@ function describeScope(dep: AgnetDeployment): string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectResourceGrants(dep: AgnetDeployment): Record<string, unknown>[] {
|
||||||
|
return (
|
||||||
|
dep.orchestration_plan?.agents
|
||||||
|
?.flatMap((agent) => agent.resource_grants || [])
|
||||||
|
.map((grant) => grant as Record<string, unknown>) ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeSecretRefs(dep: AgnetDeployment): string {
|
||||||
|
const grants = collectResourceGrants(dep)
|
||||||
|
const refs = grants.filter((grant) => {
|
||||||
|
const secretRef = grant.secret_ref
|
||||||
|
return typeof secretRef === 'string' && secretRef.trim() !== ''
|
||||||
|
})
|
||||||
|
|
||||||
|
if (refs.length > 0) return `${refs.length}/${grants.length || refs.length}`
|
||||||
|
return grants.length > 0 ? '0' : '—'
|
||||||
|
}
|
||||||
|
|
||||||
function formatRelativeTime(value: string | undefined): string {
|
function formatRelativeTime(value: string | undefined): string {
|
||||||
if (!value) return '—'
|
if (!value) return '—'
|
||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
@@ -235,6 +269,83 @@ function formatRelativeTime(value: string | undefined): string {
|
|||||||
return `${day}d ago`
|
return `${day}d ago`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const phase = dep.phase || dep.status
|
||||||
|
const risk = describeRiskLevel(dep)
|
||||||
|
const grants = collectResourceGrants(dep)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_52%,transparent)] p-4'>
|
||||||
|
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
||||||
|
<div className='min-w-0'>
|
||||||
|
<p className='text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground'>
|
||||||
|
{t('Run detail')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-1 truncate font-mono text-sm text-foreground'>
|
||||||
|
{dep.deployment_id}
|
||||||
|
</p>
|
||||||
|
<p className='mt-1 line-clamp-2 text-sm text-muted-foreground'>
|
||||||
|
{dep.orchestration_plan?.objective ||
|
||||||
|
dep.orchestration_plan?.template_hint ||
|
||||||
|
t('No objective')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<StatusBadge phase={phase} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-5'>
|
||||||
|
<MetaPill icon={Activity} label={t('status')} value={phase || '—'} />
|
||||||
|
<MetaPill icon={Tag} label={t('risk')} value={risk.label} />
|
||||||
|
<MetaPill icon={Coins} label={t('budget')} value={describeBudget(dep)} />
|
||||||
|
<MetaPill
|
||||||
|
icon={Building2}
|
||||||
|
label={t('scope')}
|
||||||
|
value={describeScope(dep)}
|
||||||
|
/>
|
||||||
|
<MetaPill
|
||||||
|
icon={ShieldCheck}
|
||||||
|
label='secret_ref'
|
||||||
|
value={describeSecretRefs(dep)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 grid gap-3 md:grid-cols-3'>
|
||||||
|
<div className='rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/45 p-3'>
|
||||||
|
<p className='flex items-center gap-2 text-xs font-semibold text-foreground'>
|
||||||
|
<FileSearch className='h-3.5 w-3.5 text-primary' />
|
||||||
|
{t('Permission manifest')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-2 text-xs leading-relaxed text-muted-foreground'>
|
||||||
|
{t('Run manifest hint')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-2 font-mono text-[11px] text-muted-foreground'>
|
||||||
|
{grants.length} {t('resource grants')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/45 p-3'>
|
||||||
|
<p className='flex items-center gap-2 text-xs font-semibold text-foreground'>
|
||||||
|
<GitCommit className='h-3.5 w-3.5 text-primary' />
|
||||||
|
{t('Events usage')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-2 text-xs leading-relaxed text-muted-foreground'>
|
||||||
|
{t('Run events hint')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/45 p-3'>
|
||||||
|
<p className='flex items-center gap-2 text-xs font-semibold text-foreground'>
|
||||||
|
<ShieldCheck className='h-3.5 w-3.5 text-primary' />
|
||||||
|
{t('Audit usage')}
|
||||||
|
</p>
|
||||||
|
<p className='mt-2 text-xs leading-relaxed text-muted-foreground'>
|
||||||
|
{t('Run audit hint')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Deployments page
|
// Deployments page
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -244,6 +355,7 @@ export function AgnetDeploymentsPage() {
|
|||||||
const [filter, setFilter] = useState<'all' | StatusKey>('all')
|
const [filter, setFilter] = useState<'all' | StatusKey>('all')
|
||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [selectedRunId, setSelectedRunId] = useState<string | undefined>()
|
||||||
|
|
||||||
const { data = [], isLoading } = useQuery({
|
const { data = [], isLoading } = useQuery({
|
||||||
queryKey: ['agnet', 'deployments'],
|
queryKey: ['agnet', 'deployments'],
|
||||||
@@ -272,12 +384,15 @@ export function AgnetDeploymentsPage() {
|
|||||||
})
|
})
|
||||||
}, [data, filter, keyword])
|
}, [data, filter, keyword])
|
||||||
|
|
||||||
|
const selectedRun =
|
||||||
|
filtered.find((dep) => dep.deployment_id === selectedRunId) ?? filtered[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSurface
|
<PageSurface
|
||||||
title={t('Deployments')}
|
title={t('Work / Runs')}
|
||||||
subtitle={t(
|
subtitle={t(
|
||||||
'Card-based view of every Agnet orchestration run with risk, budget, executor and live status.'
|
'Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.'
|
||||||
)}
|
)}
|
||||||
toolbar={
|
toolbar={
|
||||||
<>
|
<>
|
||||||
@@ -288,14 +403,14 @@ export function AgnetDeploymentsPage() {
|
|||||||
onClick={() => setCreateOpen(true)}
|
onClick={() => setCreateOpen(true)}
|
||||||
>
|
>
|
||||||
<Plus className='h-3.5 w-3.5' />
|
<Plus className='h-3.5 w-3.5' />
|
||||||
{t('New deployment')}
|
{t('New run')}
|
||||||
</Button>
|
</Button>
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<Search className='pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground' />
|
<Search className='pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground' />
|
||||||
<Input
|
<Input
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={(e) => setKeyword(e.target.value)}
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
placeholder={t('Find deployment / scope / objective')}
|
placeholder={t('Find run / scope / objective')}
|
||||||
className='h-9 w-64 rounded-xl pl-8 text-xs'
|
className='h-9 w-64 rounded-xl pl-8 text-xs'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,71 +437,99 @@ export function AgnetDeploymentsPage() {
|
|||||||
<LoadingGrid rows={4} height='h-32' />
|
<LoadingGrid rows={4} height='h-32' />
|
||||||
) : filtered.length === 0 ? (
|
) : filtered.length === 0 ? (
|
||||||
<EmptySurface
|
<EmptySurface
|
||||||
title={t('No deployments match the current filter')}
|
title={t('No runs match the current filter')}
|
||||||
hint={t('Adjust filters or trigger a new orchestration plan.')}
|
hint={t('Adjust filters or trigger a new orchestration plan.')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className='grid gap-3 sm:grid-cols-2'>
|
<div className='space-y-4'>
|
||||||
{filtered.map((dep) => {
|
{selectedRun && <RunDetailPanel dep={selectedRun} />}
|
||||||
const risk = describeRiskLevel(dep)
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
const phase = dep.phase || dep.status
|
{filtered.map((dep) => {
|
||||||
const objective =
|
const risk = describeRiskLevel(dep)
|
||||||
dep.orchestration_plan?.objective ||
|
const phase = dep.phase || dep.status
|
||||||
dep.orchestration_plan?.template_hint ||
|
const objective =
|
||||||
t('No objective')
|
dep.orchestration_plan?.objective ||
|
||||||
return (
|
dep.orchestration_plan?.template_hint ||
|
||||||
<article
|
t('No objective')
|
||||||
key={dep.deployment_id}
|
const selected = dep.deployment_id === selectedRun?.deployment_id
|
||||||
className='group flex flex-col 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:border-primary/45'
|
return (
|
||||||
>
|
<article
|
||||||
<header className='flex items-start justify-between gap-2'>
|
key={dep.deployment_id}
|
||||||
<div className='min-w-0'>
|
tabIndex={0}
|
||||||
<div className='flex items-center gap-2'>
|
role='button'
|
||||||
<Rocket className='h-3.5 w-3.5 text-primary' />
|
aria-pressed={selected}
|
||||||
<span className='font-mono text-xs'>
|
onClick={() => setSelectedRunId(dep.deployment_id)}
|
||||||
{dep.deployment_id}
|
onKeyDown={(event) => {
|
||||||
</span>
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault()
|
||||||
|
setSelectedRunId(dep.deployment_id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'group flex cursor-pointer flex-col gap-3 rounded-2xl border bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:border-primary/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||||
|
selected
|
||||||
|
? 'border-primary/55'
|
||||||
|
: 'border-[color-mix(in_oklch,var(--primary)_18%,var(--border))]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<header className='flex items-start justify-between gap-2'>
|
||||||
|
<div className='min-w-0'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Rocket className='h-3.5 w-3.5 text-primary' />
|
||||||
|
<span className='font-mono text-xs'>
|
||||||
|
{dep.deployment_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className='mt-1.5 line-clamp-2 text-sm font-medium'>
|
||||||
|
{objective}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className='mt-1.5 line-clamp-2 text-sm font-medium'>
|
<StatusBadge phase={phase} />
|
||||||
{objective}
|
</header>
|
||||||
</p>
|
|
||||||
|
<div className='flex flex-wrap gap-1.5'>
|
||||||
|
<MetaPill
|
||||||
|
icon={Tag}
|
||||||
|
label={t('risk')}
|
||||||
|
value={risk.label}
|
||||||
|
/>
|
||||||
|
<MetaPill
|
||||||
|
icon={Coins}
|
||||||
|
label={t('budget')}
|
||||||
|
value={describeBudget(dep)}
|
||||||
|
/>
|
||||||
|
<MetaPill
|
||||||
|
icon={Building2}
|
||||||
|
label={t('scope')}
|
||||||
|
value={describeScope(dep)}
|
||||||
|
/>
|
||||||
|
<MetaPill
|
||||||
|
icon={ShieldCheck}
|
||||||
|
label='secret_ref'
|
||||||
|
value={describeSecretRefs(dep)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge phase={phase} />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className='flex flex-wrap gap-1.5'>
|
<footer className='mt-auto flex items-center justify-between border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-3'>
|
||||||
<MetaPill icon={Tag} label={t('risk')} value={risk.label} />
|
<span className='text-[11px] text-muted-foreground'>
|
||||||
<MetaPill
|
{formatRelativeTime(dep.updated_at || dep.created_at)}
|
||||||
icon={Coins}
|
</span>
|
||||||
label={t('budget')}
|
<Button
|
||||||
value={describeBudget(dep)}
|
asChild
|
||||||
/>
|
size='sm'
|
||||||
<MetaPill
|
variant='ghost'
|
||||||
icon={Building2}
|
className='gap-1 text-primary'
|
||||||
label={t('scope')}
|
>
|
||||||
value={describeScope(dep)}
|
<Link to='/events'>
|
||||||
/>
|
{t('Inspect events')}
|
||||||
</div>
|
<ArrowUpRight className='h-3.5 w-3.5' />
|
||||||
|
</Link>
|
||||||
<footer className='mt-auto flex items-center justify-between border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-3'>
|
</Button>
|
||||||
<span className='text-[11px] text-muted-foreground'>
|
</footer>
|
||||||
{formatRelativeTime(dep.updated_at || dep.created_at)}
|
</article>
|
||||||
</span>
|
)
|
||||||
<Button
|
})}
|
||||||
asChild
|
</div>
|
||||||
size='sm'
|
|
||||||
variant='ghost'
|
|
||||||
className='gap-1 text-primary'
|
|
||||||
>
|
|
||||||
<Link to='/events'>
|
|
||||||
{t('Inspect events')}
|
|
||||||
<ArrowUpRight className='h-3.5 w-3.5' />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PageSurface>
|
</PageSurface>
|
||||||
@@ -684,7 +827,8 @@ export function AgnetAuditPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Git sources — 绑定代码/SK 仓库与云上权限 → 部署 → 展示各部署的快照锚点(SK 正文仍以 Git 为准)
|
// Resources — keep Git-source API wiring while presenting it as the first
|
||||||
|
// resource-binding slice for Work/Runs.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export function AgnetSKSourcesPage() {
|
export function AgnetSKSourcesPage() {
|
||||||
@@ -758,8 +902,8 @@ export function AgnetSKSourcesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSurface
|
<PageSurface
|
||||||
title={t('Git sources')}
|
title={t('Resources')}
|
||||||
subtitle={t('Git sources subtitle')}
|
subtitle={t('Resources subtitle')}
|
||||||
toolbar={
|
toolbar={
|
||||||
<Select
|
<Select
|
||||||
value={effectiveDeployment ?? ''}
|
value={effectiveDeployment ?? ''}
|
||||||
@@ -785,10 +929,10 @@ export function AgnetSKSourcesPage() {
|
|||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div>
|
<div>
|
||||||
<p className='text-sm font-medium text-foreground'>
|
<p className='text-sm font-medium text-foreground'>
|
||||||
{t('Bind Git source')}
|
{t('Bind resource source')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-1 text-xs text-muted-foreground'>
|
<p className='mt-1 text-xs text-muted-foreground'>
|
||||||
{t('Bind Git source description')}
|
{t('Bind resource source description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<GitBranch className='h-5 w-5 text-primary' />
|
<GitBranch className='h-5 w-5 text-primary' />
|
||||||
@@ -933,7 +1077,7 @@ export function AgnetSKSourcesPage() {
|
|||||||
|
|
||||||
<div className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4'>
|
<div className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4'>
|
||||||
<p className='text-sm font-medium text-foreground'>
|
<p className='text-sm font-medium text-foreground'>
|
||||||
{t('Bound Git sources')}
|
{t('Bound resource sources')}
|
||||||
</p>
|
</p>
|
||||||
{gitSourcesQuery.isLoading ? (
|
{gitSourcesQuery.isLoading ? (
|
||||||
<div className='mt-3 space-y-2'>
|
<div className='mt-3 space-y-2'>
|
||||||
@@ -942,7 +1086,7 @@ export function AgnetSKSourcesPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : gitSources.length === 0 ? (
|
) : gitSources.length === 0 ? (
|
||||||
<p className='mt-3 text-xs text-muted-foreground'>
|
<p className='mt-3 text-xs text-muted-foreground'>
|
||||||
{t('No Git sources bound yet')}
|
{t('No resource sources bound yet')}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className='mt-3 space-y-2'>
|
<ul className='mt-3 space-y-2'>
|
||||||
@@ -986,20 +1130,20 @@ export function AgnetSKSourcesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4 text-sm leading-relaxed text-muted-foreground'>
|
<div className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4 text-sm leading-relaxed text-muted-foreground'>
|
||||||
<p className='font-medium text-foreground'>{t('Git binding')}</p>
|
<p className='font-medium text-foreground'>{t('Resource binding')}</p>
|
||||||
<p className='mt-2'>{t('Git sources binding explainer')}</p>
|
<p className='mt-2'>{t('Resources binding explainer')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4 text-sm leading-relaxed text-muted-foreground'>
|
<div className='mt-4 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4 text-sm leading-relaxed text-muted-foreground'>
|
||||||
<p className='font-medium text-foreground'>
|
<p className='font-medium text-foreground'>
|
||||||
{t('Git sources workflow title')}
|
{t('Resources workflow title')}
|
||||||
</p>
|
</p>
|
||||||
<ol className='mt-3 list-decimal space-y-2 ps-5 marker:text-muted-foreground'>
|
<ol className='mt-3 list-decimal space-y-2 ps-5 marker:text-muted-foreground'>
|
||||||
<li>{t('Git sources workflow step 1')}</li>
|
<li>{t('Resources workflow step 1')}</li>
|
||||||
<li>{t('Git sources workflow step 2')}</li>
|
<li>{t('Resources workflow step 2')}</li>
|
||||||
<li>{t('Git sources workflow step 3')}</li>
|
<li>{t('Resources workflow step 3')}</li>
|
||||||
<li>{t('Git sources workflow step 4')}</li>
|
<li>{t('Resources workflow step 4')}</li>
|
||||||
<li>{t('Git sources workflow step 5')}</li>
|
<li>{t('Resources workflow step 5')}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+48
-1
@@ -3870,6 +3870,53 @@
|
|||||||
"Your Turnstile site key": "Your Turnstile site key",
|
"Your Turnstile site key": "Your Turnstile site key",
|
||||||
"Zhipu": "Zhipu",
|
"Zhipu": "Zhipu",
|
||||||
"Zhipu V4": "Zhipu V4",
|
"Zhipu V4": "Zhipu V4",
|
||||||
"Zoom": "Zoom"
|
"Zoom": "Zoom",
|
||||||
|
"Resource permission scope is required": "Resource permission scope is required",
|
||||||
|
"Secret ref is required for credential resources": "Secret ref is required for credential resources",
|
||||||
|
"New work": "New work",
|
||||||
|
"Create a deployment from an idea, bounded resources, role cards, and a permission manifest.": "Create a deployment from an idea, bounded resources, role cards, and a permission manifest.",
|
||||||
|
"Idea": "Idea",
|
||||||
|
"Roles": "Roles",
|
||||||
|
"Review": "Review",
|
||||||
|
"Idea and objective": "Idea and objective",
|
||||||
|
"Start with the work outcome. Technical fields stay in the generated deployment plan.": "Start with the work outcome. Technical fields stay in the generated deployment plan.",
|
||||||
|
"roles": "roles",
|
||||||
|
"Describe what this work should achieve.": "Describe what this work should achieve.",
|
||||||
|
"Resource scope": "Resource scope",
|
||||||
|
"Scope is metadata and secret_ref only. Plaintext credentials do not belong in this form.": "Scope is metadata and secret_ref only. Plaintext credentials do not belong in this form.",
|
||||||
|
"Role cards": "Role cards",
|
||||||
|
"Each card maps one child Agnet role to a runtime model and one bounded resource grant.": "Each card maps one child Agnet role to a runtime model and one bounded resource grant.",
|
||||||
|
"Resource grant": "Resource grant",
|
||||||
|
"Permissions and manifest": "Permissions and manifest",
|
||||||
|
"Review the runtime model and resource grants before submitting.": "Review the runtime model and resource grants before submitting.",
|
||||||
|
"I confirm this manifest grants only the listed resources and contains no plaintext credentials.": "I confirm this manifest grants only the listed resources and contains no plaintext credentials.",
|
||||||
|
"Run detail": "Run detail",
|
||||||
|
"status": "status",
|
||||||
|
"Permission manifest": "Permission manifest",
|
||||||
|
"Run manifest hint": "Use the manifest as the structured contract: resource grants, scope, budget, runtime policy, and secret_ref references only.",
|
||||||
|
"resource grants": "resource grants",
|
||||||
|
"Events usage": "Events usage",
|
||||||
|
"Run events hint": "Open events when a run changes phase, a policy blocks execution, or a snapshot anchor needs correlation.",
|
||||||
|
"Audit usage": "Audit usage",
|
||||||
|
"Run audit hint": "Use audit to answer who approved or changed the run, which scope was used, and when a Resource Grant should be revoked.",
|
||||||
|
"Work / Runs": "Work / Runs",
|
||||||
|
"Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.": "Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.",
|
||||||
|
"New run": "New run",
|
||||||
|
"Find run / scope / objective": "Find run / scope / objective",
|
||||||
|
"No runs match the current filter": "No runs match the current filter",
|
||||||
|
"Resources": "Resources",
|
||||||
|
"Resources subtitle": "Bind resource sources, then inspect the snapshot anchors each Work/Run resolved for manifest and audit replay.",
|
||||||
|
"Bind resource source": "Bind resource source",
|
||||||
|
"Bind resource source description": "Register the Git-backed project, SK, or combined resource slice that Work/Runs can turn into scoped Resource Grants.",
|
||||||
|
"Bound resource sources": "Bound resource sources",
|
||||||
|
"No resource sources bound yet": "No resource sources bound yet.",
|
||||||
|
"Resource binding": "Resource binding",
|
||||||
|
"Resources binding explainer": "Resources are the user-visible bindings behind runs: code repos, SK repos, project documents, cloud accounts, and cloud resources. This page still uses the Git-source API slice, but the operating model is Resource Binding plus Resource Grant: metadata, scope, status, and secret_ref only.",
|
||||||
|
"Resources workflow title": "Resource-to-run flow",
|
||||||
|
"Resources workflow step 1": "Bind project code, SK, document, or cloud-resource metadata without exposing raw credentials.",
|
||||||
|
"Resources workflow step 2": "Keep secret material behind secret_ref; Manager surfaces references and status only.",
|
||||||
|
"Resources workflow step 3": "Allocate scope, allowed paths/actions, runtime policy, and budget to the run manifest.",
|
||||||
|
"Resources workflow step 4": "Start a Work/Run; Agnet resolves immutable anchors and enforces the effective grants.",
|
||||||
|
"Resources workflow step 5": "Use snapshots, events, and audit together to replay which resource context actually ran."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-1
@@ -3870,6 +3870,53 @@
|
|||||||
"Your Turnstile site key": "您的 Turnstile 站点密钥",
|
"Your Turnstile site key": "您的 Turnstile 站点密钥",
|
||||||
"Zhipu": "智谱",
|
"Zhipu": "智谱",
|
||||||
"Zhipu V4": "智谱 V4",
|
"Zhipu V4": "智谱 V4",
|
||||||
"Zoom": "缩放"
|
"Zoom": "缩放",
|
||||||
|
"Resource permission scope is required": "资源权限范围为必填项",
|
||||||
|
"Secret ref is required for credential resources": "凭据型资源必须填写 secret_ref",
|
||||||
|
"New work": "新工作",
|
||||||
|
"Create a deployment from an idea, bounded resources, role cards, and a permission manifest.": "从想法、受限资源、角色卡和权限清单创建一次部署。",
|
||||||
|
"Idea": "想法",
|
||||||
|
"Roles": "角色",
|
||||||
|
"Review": "确认",
|
||||||
|
"Idea and objective": "想法与目标",
|
||||||
|
"Start with the work outcome. Technical fields stay in the generated deployment plan.": "先描述工作结果,技术字段会进入生成的部署计划。",
|
||||||
|
"roles": "个角色",
|
||||||
|
"Describe what this work should achieve.": "描述这次工作要达成什么。",
|
||||||
|
"Resource scope": "资源范围",
|
||||||
|
"Scope is metadata and secret_ref only. Plaintext credentials do not belong in this form.": "这里只填写 metadata 和 secret_ref,不能填写明文凭据。",
|
||||||
|
"Role cards": "角色卡",
|
||||||
|
"Each card maps one child Agnet role to a runtime model and one bounded resource grant.": "每张卡把一个子 Agnet 角色映射到运行模型和一个受限资源授权。",
|
||||||
|
"Resource grant": "资源授权",
|
||||||
|
"Permissions and manifest": "权限与清单",
|
||||||
|
"Review the runtime model and resource grants before submitting.": "提交前确认运行模型与资源授权。",
|
||||||
|
"I confirm this manifest grants only the listed resources and contains no plaintext credentials.": "我确认该清单仅授权列出的资源,且不包含明文凭据。",
|
||||||
|
"Run detail": "运行详情",
|
||||||
|
"status": "状态",
|
||||||
|
"Permission manifest": "权限 manifest",
|
||||||
|
"Run manifest hint": "把 manifest 作为结构化契约查看:资源授权、范围、预算、运行策略和 secret_ref 引用。",
|
||||||
|
"resource grants": "资源授权",
|
||||||
|
"Events usage": "事件用法",
|
||||||
|
"Run events hint": "运行变更阶段、策略阻断执行或需要关联快照锚点时查看事件。",
|
||||||
|
"Audit usage": "审计用法",
|
||||||
|
"Run audit hint": "用审计回答谁批准或修改了运行、使用了哪个范围,以及何时应撤销 Resource Grant。",
|
||||||
|
"Work / Runs": "工作 / 运行",
|
||||||
|
"Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.": "按状态、风险、预算、作用域和 secret_ref 覆盖情况跟踪每次 Agnet 工作运行。",
|
||||||
|
"New run": "新运行",
|
||||||
|
"Find run / scope / objective": "查找运行 / 作用域 / 目标",
|
||||||
|
"No runs match the current filter": "没有匹配当前筛选条件的运行",
|
||||||
|
"Resources": "资源",
|
||||||
|
"Resources subtitle": "绑定资源来源,并查看每次 Work/Run 为 manifest 与审计回放解析出的快照锚点。",
|
||||||
|
"Bind resource source": "绑定资源来源",
|
||||||
|
"Bind resource source description": "登记 Work/Runs 可转成带作用域 Resource Grant 的 Git 项目、SK 或二合一资源切片。",
|
||||||
|
"Bound resource sources": "已绑定资源来源",
|
||||||
|
"No resource sources bound yet": "还没有绑定资源来源。",
|
||||||
|
"Resource binding": "资源绑定",
|
||||||
|
"Resources binding explainer": "Resources 是运行背后的用户可见绑定:代码仓库、SK 仓库、项目文档、云账号和云资源。本页仍复用 Git source API 切片,但运行模型是 Resource Binding 加 Resource Grant:只包含 metadata、scope、status 和 secret_ref。",
|
||||||
|
"Resources workflow title": "资源到运行流程",
|
||||||
|
"Resources workflow step 1": "绑定项目代码、SK、文档或云资源 metadata,不暴露原始凭据。",
|
||||||
|
"Resources workflow step 2": "密钥材料留在 secret_ref 后面;Manager 只展示引用和状态。",
|
||||||
|
"Resources workflow step 3": "为运行 manifest 分配范围、允许路径/动作、运行策略和预算。",
|
||||||
|
"Resources workflow step 4": "启动 Work/Run;Agnet 解析不可变锚点并执行有效授权。",
|
||||||
|
"Resources workflow step 5": "结合快照、事件和审计回放实际运行的资源上下文。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user