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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 23:45:10 +08:00
co-authored by Claude Opus 4.8
parent 12602ebcd6
commit 0fe1d20d67
112 changed files with 4305 additions and 3139 deletions
@@ -13,20 +13,20 @@ import (
)
const (
agnetApprovalStatusPending = "pending"
agnetApprovalStatusApproved = "approved"
agnetApprovalStatusRejected = "rejected"
agnetApprovalStatusExpired = "expired"
agentApprovalStatusPending = "pending"
agentApprovalStatusApproved = "approved"
agentApprovalStatusRejected = "rejected"
agentApprovalStatusExpired = "expired"
agnetLeaseStatusActive = "active"
agnetLeaseStatusExpired = "expired"
agnetLeaseStatusRevoked = "revoked"
agentLeaseStatusActive = "active"
agentLeaseStatusExpired = "expired"
agentLeaseStatusRevoked = "revoked"
defaultAgnetApprovalTTLSeconds = 15 * 60
maxAgnetApprovalTTLSeconds = 60 * 60
defaultAgentApprovalTTLSeconds = 15 * 60
maxAgentApprovalTTLSeconds = 60 * 60
)
type agnetApprovalPayload struct {
type agentApprovalPayload struct {
DeploymentID string `json:"deployment_id"`
BindingScope string `json:"binding_scope"`
Operation string `json:"operation"`
@@ -41,11 +41,11 @@ type agnetApprovalPayload struct {
Reason string `json:"reason"`
}
type agnetDecisionPayload struct {
type agentDecisionPayload struct {
Reason string `json:"reason"`
}
type agnetApprovalResponse struct {
type agentApprovalResponse struct {
ApprovalID string `json:"approval_id"`
UserId int `json:"user_id"`
DeploymentID string `json:"deployment_id"`
@@ -68,10 +68,10 @@ type agnetApprovalResponse struct {
DecidedAt int64 `json:"decided_at,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
CredentialLease *agnetCredentialLeaseResponse `json:"credential_lease,omitempty"`
CredentialLease *agentCredentialLeaseResponse `json:"credential_lease,omitempty"`
}
type agnetCredentialLeaseResponse struct {
type agentCredentialLeaseResponse struct {
LeaseID string `json:"lease_id"`
CredentialRef string `json:"credential_ref"`
ApprovalID string `json:"approval_id"`
@@ -90,13 +90,13 @@ type agnetCredentialLeaseResponse struct {
UpdatedAt int64 `json:"updated_at"`
}
func CreateAgnetApprovalRequest(c *gin.Context) {
var payload agnetApprovalPayload
func CreateAgentApprovalRequest(c *gin.Context) {
var payload agentApprovalPayload
if err := common.DecodeJson(c.Request.Body, &payload); err != nil {
common.ApiError(c, err)
return
}
payload, err := normalizeAgnetApprovalPayload(payload)
payload, err := normalizeAgentApprovalPayload(payload)
if err != nil {
common.ApiError(c, err)
return
@@ -109,7 +109,7 @@ func CreateAgnetApprovalRequest(c *gin.Context) {
}
now := time.Now().UnixMilli()
approval := model.AgnetApprovalRequest{
approval := model.AgentApprovalRequest{
ApprovalID: "appr_" + common.GetUUID(),
UserId: userID,
DeploymentID: payload.DeploymentID,
@@ -122,8 +122,8 @@ func CreateAgnetApprovalRequest(c *gin.Context) {
RiskLevel: payload.RiskLevel,
RequiresCredential: payload.RequiresCredential,
SecretRef: payload.SecretRef,
Status: agnetApprovalStatusPending,
RequestedBy: agnetActorForUser(userID),
Status: agentApprovalStatusPending,
RequestedBy: agentActorForUser(userID),
RequestReason: payload.Reason,
TTLSeconds: payload.TTLSeconds,
ExpiresAt: now + int64(payload.TTLSeconds)*1000,
@@ -132,11 +132,11 @@ func CreateAgnetApprovalRequest(c *gin.Context) {
common.ApiError(c, err)
return
}
recordAgnetApprovalAudit("approval.requested", &approval, nil, "ok", "")
common.ApiSuccess(c, agnetApprovalToResponse(approval, nil))
recordAgentApprovalAudit("approval.requested", &approval, nil, "ok", "")
common.ApiSuccess(c, agentApprovalToResponse(approval, nil))
}
func ListAgnetApprovalRequests(c *gin.Context) {
func ListAgentApprovalRequests(c *gin.Context) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
@@ -151,58 +151,58 @@ func ListAgnetApprovalRequests(c *gin.Context) {
q = q.Where("deployment_id = ?", deploymentID)
}
var approvals []model.AgnetApprovalRequest
var approvals []model.AgentApprovalRequest
if err := q.Order("created_at desc, id desc").Limit(200).Find(&approvals).Error; err != nil {
common.ApiError(c, err)
return
}
for i := range approvals {
expireAgnetApprovalIfNeeded(&approvals[i])
expireAgentApprovalIfNeeded(&approvals[i])
}
items := make([]agnetApprovalResponse, 0, len(approvals))
items := make([]agentApprovalResponse, 0, len(approvals))
for _, approval := range approvals {
if statusFilter != "" && approval.Status != statusFilter {
continue
}
items = append(items, agnetApprovalToResponse(approval, nil))
items = append(items, agentApprovalToResponse(approval, nil))
}
common.ApiSuccess(c, gin.H{"items": items})
}
func GetAgnetApprovalRequest(c *gin.Context) {
approval, ok := findAgnetApprovalForUser(c)
func GetAgentApprovalRequest(c *gin.Context) {
approval, ok := findAgentApprovalForUser(c)
if !ok {
return
}
expireAgnetApprovalIfNeeded(&approval)
lease := findAgnetCredentialLeaseByApproval(approval.ApprovalID)
common.ApiSuccess(c, agnetApprovalToResponse(approval, lease))
expireAgentApprovalIfNeeded(&approval)
lease := findAgentCredentialLeaseByApproval(approval.ApprovalID)
common.ApiSuccess(c, agentApprovalToResponse(approval, lease))
}
func ApproveAgnetApprovalRequest(c *gin.Context) {
approval, ok := findAgnetApprovalForUser(c)
func ApproveAgentApprovalRequest(c *gin.Context) {
approval, ok := findAgentApprovalForUser(c)
if !ok {
return
}
if expireAgnetApprovalIfNeeded(&approval) {
if expireAgentApprovalIfNeeded(&approval) {
common.ApiErrorMsg(c, "approval request expired")
return
}
if approval.Status != agnetApprovalStatusPending {
if approval.Status != agentApprovalStatusPending {
common.ApiErrorMsg(c, "approval request is not pending")
return
}
var payload agnetDecisionPayload
var payload agentDecisionPayload
_ = common.DecodeJson(c.Request.Body, &payload)
now := time.Now().UnixMilli()
approval.Status = agnetApprovalStatusApproved
approval.DecidedBy = agnetActorForUser(c.GetInt("id"))
approval.Status = agentApprovalStatusApproved
approval.DecidedBy = agentActorForUser(c.GetInt("id"))
approval.DecisionReason = strings.TrimSpace(payload.Reason)
approval.DecidedAt = now
var lease *model.AgnetCredentialLease
var lease *model.AgentCredentialLease
err := model.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&approval).Error; err != nil {
return err
@@ -210,7 +210,7 @@ func ApproveAgnetApprovalRequest(c *gin.Context) {
if !approval.RequiresCredential {
return nil
}
createdLease := model.AgnetCredentialLease{
createdLease := model.AgentCredentialLease{
LeaseID: "lease_" + common.GetUUID(),
ApprovalID: approval.ApprovalID,
UserId: approval.UserId,
@@ -221,11 +221,11 @@ func ApproveAgnetApprovalRequest(c *gin.Context) {
ResourceScope: approval.ResourceScope,
TargetRole: approval.TargetRole,
SecretRef: approval.SecretRef,
Status: agnetLeaseStatusActive,
Status: agentLeaseStatusActive,
TTLSeconds: approval.TTLSeconds,
ExpiresAt: now + int64(approval.TTLSeconds)*1000,
}
createdLease.CredentialRef = "lease://agnet/" + createdLease.LeaseID
createdLease.CredentialRef = "lease://agent/" + createdLease.LeaseID
if err := tx.Create(&createdLease).Error; err != nil {
return err
}
@@ -240,44 +240,44 @@ func ApproveAgnetApprovalRequest(c *gin.Context) {
common.ApiError(c, err)
return
}
recordAgnetApprovalAudit("approval.approved", &approval, nil, "ok", "")
recordAgentApprovalAudit("approval.approved", &approval, nil, "ok", "")
if lease != nil {
recordAgnetApprovalAudit("credential_lease.created", &approval, lease, "ok", "")
recordAgentApprovalAudit("credential_lease.created", &approval, lease, "ok", "")
}
syncAgnetRuntimeApprovalDecision(c, &approval, lease, agnetApprovalStatusApproved)
common.ApiSuccess(c, agnetApprovalToResponse(approval, lease))
syncAgentRuntimeApprovalDecision(c, &approval, lease, agentApprovalStatusApproved)
common.ApiSuccess(c, agentApprovalToResponse(approval, lease))
}
func RejectAgnetApprovalRequest(c *gin.Context) {
approval, ok := findAgnetApprovalForUser(c)
func RejectAgentApprovalRequest(c *gin.Context) {
approval, ok := findAgentApprovalForUser(c)
if !ok {
return
}
if expireAgnetApprovalIfNeeded(&approval) {
if expireAgentApprovalIfNeeded(&approval) {
common.ApiErrorMsg(c, "approval request expired")
return
}
if approval.Status != agnetApprovalStatusPending {
if approval.Status != agentApprovalStatusPending {
common.ApiErrorMsg(c, "approval request is not pending")
return
}
var payload agnetDecisionPayload
var payload agentDecisionPayload
_ = common.DecodeJson(c.Request.Body, &payload)
approval.Status = agnetApprovalStatusRejected
approval.DecidedBy = agnetActorForUser(c.GetInt("id"))
approval.Status = agentApprovalStatusRejected
approval.DecidedBy = agentActorForUser(c.GetInt("id"))
approval.DecisionReason = strings.TrimSpace(payload.Reason)
approval.DecidedAt = time.Now().UnixMilli()
if err := model.DB.Save(&approval).Error; err != nil {
common.ApiError(c, err)
return
}
recordAgnetApprovalAudit("approval.rejected", &approval, nil, "ok", "")
syncAgnetRuntimeApprovalDecision(c, &approval, nil, agnetApprovalStatusRejected)
common.ApiSuccess(c, agnetApprovalToResponse(approval, nil))
recordAgentApprovalAudit("approval.rejected", &approval, nil, "ok", "")
syncAgentRuntimeApprovalDecision(c, &approval, nil, agentApprovalStatusRejected)
common.ApiSuccess(c, agentApprovalToResponse(approval, nil))
}
func ListAgnetCredentialLeases(c *gin.Context) {
func ListAgentCredentialLeases(c *gin.Context) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
@@ -292,23 +292,23 @@ func ListAgnetCredentialLeases(c *gin.Context) {
q = q.Where("deployment_id = ?", deploymentID)
}
var leases []model.AgnetCredentialLease
var leases []model.AgentCredentialLease
if err := q.Order("created_at desc, id desc").Limit(200).Find(&leases).Error; err != nil {
common.ApiError(c, err)
return
}
items := make([]agnetCredentialLeaseResponse, 0, len(leases))
items := make([]agentCredentialLeaseResponse, 0, len(leases))
for i := range leases {
expireAgnetCredentialLeaseIfNeeded(&leases[i])
expireAgentCredentialLeaseIfNeeded(&leases[i])
if statusFilter != "" && leases[i].Status != statusFilter {
continue
}
items = append(items, agnetLeaseToResponse(leases[i]))
items = append(items, agentLeaseToResponse(leases[i]))
}
common.ApiSuccess(c, gin.H{"items": items})
}
func RevokeAgnetCredentialLease(c *gin.Context) {
func RevokeAgentCredentialLease(c *gin.Context) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
@@ -319,7 +319,7 @@ func RevokeAgnetCredentialLease(c *gin.Context) {
common.ApiErrorMsg(c, "lease_id required")
return
}
var lease model.AgnetCredentialLease
var lease model.AgentCredentialLease
if err := model.DB.Where("lease_id = ? AND user_id = ?", leaseID, userID).First(&lease).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "credential lease not found")
@@ -328,28 +328,28 @@ func RevokeAgnetCredentialLease(c *gin.Context) {
common.ApiError(c, err)
return
}
if expireAgnetCredentialLeaseIfNeeded(&lease) {
common.ApiSuccess(c, agnetLeaseToResponse(lease))
if expireAgentCredentialLeaseIfNeeded(&lease) {
common.ApiSuccess(c, agentLeaseToResponse(lease))
return
}
if lease.Status != agnetLeaseStatusActive {
if lease.Status != agentLeaseStatusActive {
common.ApiErrorMsg(c, "credential lease is not active")
return
}
lease.Status = agnetLeaseStatusRevoked
lease.Status = agentLeaseStatusRevoked
lease.RevokedAt = time.Now().UnixMilli()
if err := model.DB.Save(&lease).Error; err != nil {
common.ApiError(c, err)
return
}
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ?", lease.ApprovalID).First(&approval).Error; err == nil {
recordAgnetApprovalAudit("credential_lease.revoked", &approval, &lease, "ok", "")
recordAgentApprovalAudit("credential_lease.revoked", &approval, &lease, "ok", "")
}
common.ApiSuccess(c, agnetLeaseToResponse(lease))
common.ApiSuccess(c, agentLeaseToResponse(lease))
}
func normalizeAgnetApprovalPayload(p agnetApprovalPayload) (agnetApprovalPayload, error) {
func normalizeAgentApprovalPayload(p agentApprovalPayload) (agentApprovalPayload, error) {
p.DeploymentID = strings.TrimSpace(p.DeploymentID)
p.BindingScope = strings.TrimSpace(p.BindingScope)
p.Operation = strings.TrimSpace(p.Operation)
@@ -380,10 +380,10 @@ func normalizeAgnetApprovalPayload(p agnetApprovalPayload) (agnetApprovalPayload
return p, errors.New("risk_level must be low, medium, high, or critical")
}
if p.TTLSeconds <= 0 {
p.TTLSeconds = defaultAgnetApprovalTTLSeconds
p.TTLSeconds = defaultAgentApprovalTTLSeconds
}
if p.TTLSeconds > maxAgnetApprovalTTLSeconds {
p.TTLSeconds = maxAgnetApprovalTTLSeconds
if p.TTLSeconds > maxAgentApprovalTTLSeconds {
p.TTLSeconds = maxAgentApprovalTTLSeconds
}
if p.RequiresCredential {
if p.SecretRef == "" {
@@ -396,73 +396,73 @@ func normalizeAgnetApprovalPayload(p agnetApprovalPayload) (agnetApprovalPayload
return p, nil
}
func findAgnetApprovalForUser(c *gin.Context) (model.AgnetApprovalRequest, bool) {
func findAgentApprovalForUser(c *gin.Context) (model.AgentApprovalRequest, bool) {
userID := c.GetInt("id")
if userID <= 0 {
common.ApiErrorMsg(c, "user authentication required")
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
approvalID := strings.TrimSpace(c.Param("approval_id"))
if approvalID == "" {
common.ApiErrorMsg(c, "approval_id required")
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ? AND user_id = ?", approvalID, userID).First(&approval).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "approval request not found")
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
common.ApiError(c, err)
return model.AgnetApprovalRequest{}, false
return model.AgentApprovalRequest{}, false
}
return approval, true
}
func findAgnetCredentialLeaseByApproval(approvalID string) *model.AgnetCredentialLease {
var lease model.AgnetCredentialLease
func findAgentCredentialLeaseByApproval(approvalID string) *model.AgentCredentialLease {
var lease model.AgentCredentialLease
if err := model.DB.Where("approval_id = ?", approvalID).First(&lease).Error; err != nil {
return nil
}
expireAgnetCredentialLeaseIfNeeded(&lease)
expireAgentCredentialLeaseIfNeeded(&lease)
return &lease
}
func expireAgnetApprovalIfNeeded(approval *model.AgnetApprovalRequest) bool {
if approval == nil || approval.Status != agnetApprovalStatusPending {
func expireAgentApprovalIfNeeded(approval *model.AgentApprovalRequest) bool {
if approval == nil || approval.Status != agentApprovalStatusPending {
return false
}
if approval.ExpiresAt <= 0 || approval.ExpiresAt > time.Now().UnixMilli() {
return false
}
approval.Status = agnetApprovalStatusExpired
approval.Status = agentApprovalStatusExpired
approval.DecidedAt = time.Now().UnixMilli()
if err := model.DB.Save(approval).Error; err == nil {
recordAgnetApprovalAudit("approval.expired", approval, nil, "ok", "")
recordAgentApprovalAudit("approval.expired", approval, nil, "ok", "")
}
return true
}
func expireAgnetCredentialLeaseIfNeeded(lease *model.AgnetCredentialLease) bool {
if lease == nil || lease.Status != agnetLeaseStatusActive {
func expireAgentCredentialLeaseIfNeeded(lease *model.AgentCredentialLease) bool {
if lease == nil || lease.Status != agentLeaseStatusActive {
return false
}
if lease.ExpiresAt <= 0 || lease.ExpiresAt > time.Now().UnixMilli() {
return false
}
lease.Status = agnetLeaseStatusExpired
lease.Status = agentLeaseStatusExpired
if err := model.DB.Save(lease).Error; err != nil {
return false
}
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ?", lease.ApprovalID).First(&approval).Error; err == nil {
recordAgnetApprovalAudit("credential_lease.expired", &approval, lease, "ok", "")
recordAgentApprovalAudit("credential_lease.expired", &approval, lease, "ok", "")
}
return true
}
func agnetApprovalToResponse(approval model.AgnetApprovalRequest, lease *model.AgnetCredentialLease) agnetApprovalResponse {
resp := agnetApprovalResponse{
func agentApprovalToResponse(approval model.AgentApprovalRequest, lease *model.AgentCredentialLease) agentApprovalResponse {
resp := agentApprovalResponse{
ApprovalID: approval.ApprovalID,
UserId: approval.UserId,
DeploymentID: approval.DeploymentID,
@@ -487,14 +487,14 @@ func agnetApprovalToResponse(approval model.AgnetApprovalRequest, lease *model.A
UpdatedAt: approval.UpdatedAt,
}
if lease != nil {
leaseResp := agnetLeaseToResponse(*lease)
leaseResp := agentLeaseToResponse(*lease)
resp.CredentialLease = &leaseResp
}
return resp
}
func agnetLeaseToResponse(lease model.AgnetCredentialLease) agnetCredentialLeaseResponse {
return agnetCredentialLeaseResponse{
func agentLeaseToResponse(lease model.AgentCredentialLease) agentCredentialLeaseResponse {
return agentCredentialLeaseResponse{
LeaseID: lease.LeaseID,
CredentialRef: lease.CredentialRef,
ApprovalID: lease.ApprovalID,
@@ -514,7 +514,7 @@ func agnetLeaseToResponse(lease model.AgnetCredentialLease) agnetCredentialLease
}
}
func recordAgnetApprovalAudit(event string, approval *model.AgnetApprovalRequest, lease *model.AgnetCredentialLease, result string, message string) {
func recordAgentApprovalAudit(event string, approval *model.AgentApprovalRequest, lease *model.AgentCredentialLease, result string, message string) {
if approval == nil {
return
}
@@ -540,7 +540,7 @@ func recordAgnetApprovalAudit(event string, approval *model.AgnetApprovalRequest
if raw, err := common.Marshal(details); err == nil {
detailsJSON = string(raw)
}
model.InsertAgnetAuditEvent(&model.AgnetAuditEvent{
model.InsertAgentAuditEvent(&model.AgentAuditEvent{
EventID: "evt_" + common.GetUUID(),
Event: event,
Actor: "manager",
@@ -555,6 +555,6 @@ func recordAgnetApprovalAudit(event string, approval *model.AgnetApprovalRequest
})
}
func agnetActorForUser(userID int) string {
func agentActorForUser(userID int) string {
return fmt.Sprintf("user:%d", userID)
}
@@ -17,7 +17,7 @@ import (
"gorm.io/gorm"
)
func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB {
func setupAgentApprovalTestDB(t *testing.T) *gorm.DB {
t.Helper()
gin.SetMode(gin.TestMode)
common.UsingSQLite = true
@@ -31,10 +31,10 @@ func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB {
model.DB = db
model.LOG_DB = db
require.NoError(t, db.AutoMigrate(
&model.AgnetApprovalRequest{},
&model.AgnetCredentialLease{},
&model.AgnetAuditEvent{},
&model.AgnetDeployment{},
&model.AgentApprovalRequest{},
&model.AgentCredentialLease{},
&model.AgentAuditEvent{},
&model.AgentDeployment{},
))
t.Cleanup(func() {
sqlDB, err := db.DB()
@@ -45,8 +45,8 @@ func setupAgnetApprovalTestDB(t *testing.T) *gorm.DB {
return db
}
func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
db := setupAgentApprovalTestDB(t)
var runtimeBody string
var runtimeAuth string
var runtimePath string
@@ -59,12 +59,12 @@ func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"accepted"}}`))
}))
defer runtimeServer.Close()
t.Setenv("AGNET_RUNTIME_ENABLED", "true")
t.Setenv("AGNET_RUNTIME_BASE_URL", runtimeServer.URL)
t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "runtime-token")
t.Setenv("AGNET_RUNTIME_APPROVAL_DECISION_PATH", "/api/swarms/{swarm_id}/approvals/{approval_id}")
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
t.Setenv("AGENT_RUNTIME_BASE_URL", runtimeServer.URL)
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "runtime-token")
t.Setenv("AGENT_RUNTIME_APPROVAL_DECISION_PATH", "/api/swarms/{swarm_id}/approvals/{approval_id}")
require.NoError(t, db.Create(&model.AgnetDeployment{
require.NoError(t, db.Create(&model.AgentDeployment{
DeploymentID: "dep_runtime_approval",
UserID: "7",
BindingScope: "project-alpha",
@@ -90,8 +90,8 @@ func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
"ttl_seconds":600
}`
_, approvalID := createAgnetApprovalForTest(t, 7, body)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许写入"}`)
_, approvalID := createAgentApprovalForTest(t, 7, body)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许写入"}`)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "/api/swarms/swarm-runtime-1/approvals/"+approvalID, runtimePath)
require.Equal(t, "Bearer runtime-token", runtimeAuth)
@@ -100,16 +100,16 @@ func TestAgnetApprovalApproveNotifiesRuntimeDecision(t *testing.T) {
require.Contains(t, runtimeBody, `"manager_deployment_id":"dep_runtime_approval"`)
require.Contains(t, runtimeBody, `"runtime_deployment_id":"runtime-dep-1"`)
require.Contains(t, runtimeBody, `"swarm_id":"swarm-runtime-1"`)
require.Contains(t, runtimeBody, `"credential_ref":"lease://agnet/`)
require.Contains(t, runtimeBody, `"credential_ref":"lease://agent/`)
require.NotContains(t, runtimeBody, "azkv://")
var auditRows []model.AgnetAuditEvent
var auditRows []model.AgentAuditEvent
require.NoError(t, db.Where("deployment_id = ?", "dep_runtime_approval").Order("id asc").Find(&auditRows).Error)
require.NotEmpty(t, auditRows)
require.Equal(t, "runtime.approval_decision.accepted", auditRows[len(auditRows)-1].Event)
}
func performAgnetApprovalRequest(handler gin.HandlerFunc, userID int, method string, routePath string, requestPath string, body string) *httptest.ResponseRecorder {
func performAgentApprovalRequest(handler gin.HandlerFunc, userID int, method string, routePath string, requestPath string, body string) *httptest.ResponseRecorder {
r := gin.New()
r.Handle(method, routePath, func(c *gin.Context) {
c.Set("id", userID)
@@ -122,18 +122,18 @@ func performAgnetApprovalRequest(handler gin.HandlerFunc, userID int, method str
return w
}
func decodeAgnetApprovalEnvelope(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
func decodeAgentApprovalEnvelope(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
t.Helper()
var envelope map[string]any
require.NoError(t, common.Unmarshal(w.Body.Bytes(), &envelope))
return envelope
}
func createAgnetApprovalForTest(t *testing.T, userID int, body string) (map[string]any, string) {
func createAgentApprovalForTest(t *testing.T, userID int, body string) (map[string]any, string) {
t.Helper()
w := performAgnetApprovalRequest(CreateAgnetApprovalRequest, userID, http.MethodPost, "/approvals", "/approvals", body)
w := performAgentApprovalRequest(CreateAgentApprovalRequest, userID, http.MethodPost, "/approvals", "/approvals", body)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
approvalID := data["approval_id"].(string)
@@ -141,8 +141,8 @@ func createAgnetApprovalForTest(t *testing.T, userID int, body string) (map[stri
return data, approvalID
}
func TestAgnetApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_1",
"binding_scope":"project-alpha",
@@ -158,27 +158,27 @@ func TestAgnetApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
"reason":"需要写入功能分支"
}`
_, approvalID := createAgnetApprovalForTest(t, 7, body)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许本次任务"}`)
_, approvalID := createAgentApprovalForTest(t, 7, body)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 7, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{"reason":"允许本次任务"}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
require.Equal(t, "approved", data["status"])
lease := data["credential_lease"].(map[string]any)
require.Equal(t, "active", lease["status"])
require.Contains(t, lease["credential_ref"], "lease://agnet/")
require.Contains(t, lease["credential_ref"], "lease://agent/")
require.NotContains(t, w.Body.String(), "do-not-store")
require.NotContains(t, w.Body.String(), "azkv://", "API responses must not expose internal Key Vault secret_ref")
var storedLease model.AgnetCredentialLease
var storedLease model.AgentCredentialLease
require.NoError(t, db.Where("approval_id = ?", approvalID).First(&storedLease).Error)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo-main", storedLease.SecretRef)
require.Equal(t, "active", storedLease.Status)
require.Greater(t, storedLease.ExpiresAt, storedLease.CreatedAt)
require.NotContains(t, storedLease.CredentialRef, "azkv://")
var auditRows []model.AgnetAuditEvent
var auditRows []model.AgentAuditEvent
require.NoError(t, db.Order("id asc").Find(&auditRows).Error)
require.Len(t, auditRows, 3)
require.Equal(t, "approval.requested", auditRows[0].Event)
@@ -187,8 +187,8 @@ func TestAgnetApprovalApproveCreatesShortLivedLeaseAndAudit(t *testing.T) {
require.NotContains(t, auditRows[2].DetailsJSON, "repo-main-secret-value")
}
func TestAgnetApprovalRejectDoesNotCreateLease(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalRejectDoesNotCreateLease(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_2",
"binding_scope":"project-alpha",
@@ -203,21 +203,21 @@ func TestAgnetApprovalRejectDoesNotCreateLease(t *testing.T) {
"ttl_seconds":300
}`
_, approvalID := createAgnetApprovalForTest(t, 8, body)
w := performAgnetApprovalRequest(RejectAgnetApprovalRequest, 8, http.MethodPost, "/approvals/:approval_id/reject", "/approvals/"+approvalID+"/reject", `{"reason":"风险过高"}`)
_, approvalID := createAgentApprovalForTest(t, 8, body)
w := performAgentApprovalRequest(RejectAgentApprovalRequest, 8, http.MethodPost, "/approvals/:approval_id/reject", "/approvals/"+approvalID+"/reject", `{"reason":"风险过高"}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
require.Equal(t, "rejected", data["status"])
var leases int64
require.NoError(t, db.Model(&model.AgnetCredentialLease{}).Count(&leases).Error)
require.NoError(t, db.Model(&model.AgentCredentialLease{}).Count(&leases).Error)
require.Equal(t, int64(0), leases)
}
func TestAgnetApprovalExpiredRequestCannotBeApproved(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentApprovalExpiredRequestCannotBeApproved(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_3",
"binding_scope":"project-alpha",
@@ -232,24 +232,24 @@ func TestAgnetApprovalExpiredRequestCannotBeApproved(t *testing.T) {
"ttl_seconds":1
}`
_, approvalID := createAgnetApprovalForTest(t, 9, body)
require.NoError(t, db.Model(&model.AgnetApprovalRequest{}).Where("approval_id = ?", approvalID).Updates(map[string]any{
_, approvalID := createAgentApprovalForTest(t, 9, body)
require.NoError(t, db.Model(&model.AgentApprovalRequest{}).Where("approval_id = ?", approvalID).Updates(map[string]any{
"expires_at": 1,
}).Error)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 9, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 9, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, false, envelope["success"])
require.Contains(t, envelope["message"], "approval request expired")
var approval model.AgnetApprovalRequest
var approval model.AgentApprovalRequest
require.NoError(t, db.Where("approval_id = ?", approvalID).First(&approval).Error)
require.Equal(t, "expired", approval.Status)
}
func TestAgnetCredentialLeaseCanBeRevoked(t *testing.T) {
db := setupAgnetApprovalTestDB(t)
func TestAgentCredentialLeaseCanBeRevoked(t *testing.T) {
db := setupAgentApprovalTestDB(t)
body := `{
"deployment_id":"dep_approval_4",
"binding_scope":"project-alpha",
@@ -264,16 +264,16 @@ func TestAgnetCredentialLeaseCanBeRevoked(t *testing.T) {
"ttl_seconds":600
}`
_, approvalID := createAgnetApprovalForTest(t, 10, body)
w := performAgnetApprovalRequest(ApproveAgnetApprovalRequest, 10, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
_, approvalID := createAgentApprovalForTest(t, 10, body)
w := performAgentApprovalRequest(ApproveAgentApprovalRequest, 10, http.MethodPost, "/approvals/:approval_id/approve", "/approvals/"+approvalID+"/approve", `{}`)
require.Equal(t, http.StatusOK, w.Code)
var storedLease model.AgnetCredentialLease
var storedLease model.AgentCredentialLease
require.NoError(t, db.Where("approval_id = ?", approvalID).First(&storedLease).Error)
revokePath := "/credential-leases/" + storedLease.LeaseID + "/revoke"
w = performAgnetApprovalRequest(RevokeAgnetCredentialLease, 10, http.MethodPost, "/credential-leases/:lease_id/revoke", revokePath, `{"reason":"任务结束"}`)
w = performAgentApprovalRequest(RevokeAgentCredentialLease, 10, http.MethodPost, "/credential-leases/:lease_id/revoke", revokePath, `{"reason":"任务结束"}`)
require.Equal(t, http.StatusOK, w.Code)
envelope := decodeAgnetApprovalEnvelope(t, w)
envelope := decodeAgentApprovalEnvelope(t, w)
require.Equal(t, true, envelope["success"])
data := envelope["data"].(map[string]any)
require.Equal(t, "revoked", data["status"])
@@ -19,7 +19,7 @@ import (
"github.com/heicode/manager/model"
)
type agnetCallbackEnvelope struct {
type agentCallbackEnvelope struct {
EventID string `json:"event_id"`
IdempotencyKey string `json:"idempotency_key"`
EventType string `json:"event_type"`
@@ -32,10 +32,10 @@ type agnetCallbackEnvelope struct {
Source string `json:"source"`
Metadata map[string]any `json:"metadata"`
Payload map[string]any `json:"payload"`
Artifact agnetArtifactPayload `json:"artifact"`
Artifact agentArtifactPayload `json:"artifact"`
}
type agnetArtifactPayload struct {
type agentArtifactPayload struct {
ArtifactID string `json:"artifact_id"`
ArtifactType string `json:"artifact_type"`
Title string `json:"title"`
@@ -45,8 +45,8 @@ type agnetArtifactPayload struct {
Metadata map[string]any `json:"metadata"`
}
func agnetCallbackTokenFromRequest(c *gin.Context) string {
if token := strings.TrimSpace(c.GetHeader("X-Agnet-Service-Token")); token != "" {
func agentCallbackTokenFromRequest(c *gin.Context) string {
if token := strings.TrimSpace(c.GetHeader("X-Agent-Service-Token")); token != "" {
return token
}
auth := strings.TrimSpace(c.GetHeader("Authorization"))
@@ -56,25 +56,25 @@ func agnetCallbackTokenFromRequest(c *gin.Context) string {
return ""
}
func agnetCallbackSigningSecret() string {
if secret := strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_CALLBACK_SIGNING_SECRET", "")); secret != "" {
func agentCallbackSigningSecret() string {
if secret := strings.TrimSpace(common.GetEnvOrDefaultString("AGENT_CALLBACK_SIGNING_SECRET", "")); secret != "" {
return secret
}
secretRef := firstNonEmpty(
common.GetEnvOrDefaultString("AGNET_CALLBACK_SIGNING_SECRET_REF", ""),
common.GetEnvOrDefaultString("AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""),
common.GetEnvOrDefaultString("AGENT_CALLBACK_SIGNING_SECRET_REF", ""),
common.GetEnvOrDefaultString("AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""),
)
if secretRef == "" {
return ""
}
client, err := newSecretStoreClientFromEnv()
if err != nil {
common.SysLog("agnetCallbackSigningSecret: " + err.Error())
common.SysLog("agentCallbackSigningSecret: " + err.Error())
return ""
}
data, err := client.getJSONSecret(secretRef)
if err != nil {
common.SysLog("agnetCallbackSigningSecret: " + err.Error())
common.SysLog("agentCallbackSigningSecret: " + err.Error())
return ""
}
for _, key := range []string{"callback_signing_secret", "signing_secret", "secret", "value"} {
@@ -82,38 +82,38 @@ func agnetCallbackSigningSecret() string {
return value
}
}
common.SysLog("agnetCallbackSigningSecret: signing secret is missing from Azure Key Vault payload")
common.SysLog("agentCallbackSigningSecret: signing secret is missing from Azure Key Vault payload")
return ""
}
func agnetCallbackSignatureTolerance() time.Duration {
seconds := common.GetEnvOrDefault("AGNET_CALLBACK_SIGNATURE_TOLERANCE_SECONDS", 300)
func agentCallbackSignatureTolerance() time.Duration {
seconds := common.GetEnvOrDefault("AGENT_CALLBACK_SIGNATURE_TOLERANCE_SECONDS", 300)
if seconds <= 0 {
seconds = 300
}
return time.Duration(seconds) * time.Second
}
func validateAgnetCallbackHMAC(c *gin.Context, rawBody []byte, eventID string) (bool, bool) {
secret := agnetCallbackSigningSecret()
func validateAgentCallbackHMAC(c *gin.Context, rawBody []byte, eventID string) (bool, bool) {
secret := agentCallbackSigningSecret()
if secret == "" {
return false, false
}
timestamp := strings.TrimSpace(c.GetHeader("X-Agnet-Timestamp"))
signature := strings.TrimSpace(c.GetHeader("X-Agnet-Signature"))
timestamp := strings.TrimSpace(c.GetHeader("X-Agent-Timestamp"))
signature := strings.TrimSpace(c.GetHeader("X-Agent-Signature"))
if timestamp == "" || signature == "" || eventID == "" {
return false, false
}
tsMs, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
agnetError(c, "CALLBACK_UNAUTHORIZED", "invalid callback timestamp")
agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback timestamp")
return true, false
}
now := time.Now()
eventTime := time.UnixMilli(tsMs)
tolerance := agnetCallbackSignatureTolerance()
tolerance := agentCallbackSignatureTolerance()
if eventTime.Before(now.Add(-tolerance)) || eventTime.After(now.Add(tolerance)) {
agnetError(c, "CALLBACK_UNAUTHORIZED", "callback timestamp outside allowed window")
agentError(c, "CALLBACK_UNAUTHORIZED", "callback timestamp outside allowed window")
return true, false
}
payload := timestamp + "." + eventID + "." + string(rawBody)
@@ -121,32 +121,32 @@ func validateAgnetCallbackHMAC(c *gin.Context, rawBody []byte, eventID string) (
mac.Write([]byte(payload))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
agnetError(c, "CALLBACK_UNAUTHORIZED", "invalid callback signature")
agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback signature")
return true, false
}
return true, true
}
func validateAgnetCallbackAuth(c *gin.Context, rawBody []byte, eventID string) bool {
if common.GetEnvOrDefaultBool("AGNET_CALLBACK_AUTH_DISABLED", false) {
func validateAgentCallbackAuth(c *gin.Context, rawBody []byte, eventID string) bool {
if common.GetEnvOrDefaultBool("AGENT_CALLBACK_AUTH_DISABLED", false) {
return true
}
if attempted, ok := validateAgnetCallbackHMAC(c, rawBody, eventID); attempted {
if attempted, ok := validateAgentCallbackHMAC(c, rawBody, eventID); attempted {
return ok
}
expected := strings.TrimSpace(os.Getenv("AGNET_CALLBACK_TOKEN"))
expected := strings.TrimSpace(os.Getenv("AGENT_CALLBACK_TOKEN"))
if expected == "" {
agnetError(c, "CALLBACK_UNAUTHORIZED", "callback token is not configured")
agentError(c, "CALLBACK_UNAUTHORIZED", "callback token is not configured")
return false
}
if agnetCallbackTokenFromRequest(c) != expected {
agnetError(c, "CALLBACK_UNAUTHORIZED", "invalid callback service token")
if agentCallbackTokenFromRequest(c) != expected {
agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback service token")
return false
}
return true
}
func agnetCallbackHasPlaintextSecret(payload agnetCallbackEnvelope) bool {
func agentCallbackHasPlaintextSecret(payload agentCallbackEnvelope) bool {
if containsPlaintextSecret(payload.Metadata) || containsPlaintextSecret(payload.Payload) || containsPlaintextSecret(payload.Artifact.Metadata) {
return true
}
@@ -158,43 +158,43 @@ func agnetCallbackHasPlaintextSecret(payload agnetCallbackEnvelope) bool {
return containsPlaintextSecret(asMap)
}
func agnetCallbackDeploymentContext(deploymentID string, swarmID string) (agnetDeploymentRecord, bool) {
func agentCallbackDeploymentContext(deploymentID string, swarmID string) (agentDeploymentRecord, bool) {
deploymentID = strings.TrimSpace(deploymentID)
swarmID = strings.TrimSpace(swarmID)
if deploymentID != "" {
if record, ok := findAgnetDeploymentRecord(deploymentID); ok {
if record, ok := findAgentDeploymentRecord(deploymentID); ok {
return record, true
}
}
runtimeID := firstNonEmpty(swarmID, deploymentID)
if runtimeID == "" {
return agnetDeploymentRecord{}, false
return agentDeploymentRecord{}, false
}
agnetMu.RLock()
for _, record := range agnetDeployments {
agentMu.RLock()
for _, record := range agentDeployments {
if strings.TrimSpace(record.RuntimeSwarmID) == runtimeID || strings.TrimSpace(record.RuntimeDeploymentID) == runtimeID {
agnetMu.RUnlock()
agentMu.RUnlock()
return record, true
}
}
agnetMu.RUnlock()
agentMu.RUnlock()
if model.DB == nil {
return agnetDeploymentRecord{}, false
return agentDeploymentRecord{}, false
}
var row model.AgnetDeployment
var row model.AgentDeployment
if err := model.DB.Where("runtime_swarm_id = ? OR runtime_deployment_id = ?", runtimeID, runtimeID).First(&row).Error; err != nil {
return agnetDeploymentRecord{}, false
return agentDeploymentRecord{}, false
}
record, err := agnetDeploymentModelToRecord(row)
record, err := agentDeploymentModelToRecord(row)
if err != nil {
common.SysLog("agnetCallbackDeploymentContext: " + err.Error())
return agnetDeploymentRecord{}, false
common.SysLog("agentCallbackDeploymentContext: " + err.Error())
return agentDeploymentRecord{}, false
}
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
return record, true
}
@@ -232,7 +232,7 @@ func callbackMapValue(values map[string]any, key string) map[string]any {
return nil
}
func agnetCallbackEventRuntimeState(eventType string, payload map[string]any) string {
func agentCallbackEventRuntimeState(eventType string, payload map[string]any) string {
if state := callbackStringValue(payload, "status"); state != "" {
return state
}
@@ -248,7 +248,7 @@ func agnetCallbackEventRuntimeState(eventType string, payload map[string]any) st
}
}
func upsertAgnetCallbackAgentInstance(record *agnetDeploymentRecord, payload agnetCallbackEnvelope, phase string, runtimeState string) bool {
func upsertAgentCallbackAgentInstance(record *agentDeploymentRecord, payload agentCallbackEnvelope, phase string, runtimeState string) bool {
if record == nil {
return false
}
@@ -284,7 +284,7 @@ func upsertAgnetCallbackAgentInstance(record *agnetDeploymentRecord, payload agn
return true
}
}
record.AgentInstances = append(record.AgentInstances, agnetAgentInstance{
record.AgentInstances = append(record.AgentInstances, agentAgentInstance{
InstanceID: firstNonEmpty(instanceID, "agi_"+common.GetUUID()[:12]),
Role: role,
Phase: firstNonEmpty(phase, record.Phase),
@@ -294,7 +294,7 @@ func upsertAgnetCallbackAgentInstance(record *agnetDeploymentRecord, payload agn
return true
}
func applyAgnetCallbackDeploymentState(payload agnetCallbackEnvelope, record agnetDeploymentRecord) (agnetDeploymentRecord, bool) {
func applyAgentCallbackDeploymentState(payload agentCallbackEnvelope, record agentDeploymentRecord) (agentDeploymentRecord, bool) {
if strings.TrimSpace(record.DeploymentID) == "" {
return record, false
}
@@ -342,25 +342,25 @@ func applyAgnetCallbackDeploymentState(payload agnetCallbackEnvelope, record agn
}
case "agent.started", "agent.completed", "agent.crashed":
phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase"), record.Phase)
runtimeState := agnetCallbackEventRuntimeState(payload.EventType, source)
if upsertAgnetCallbackAgentInstance(&record, payload, phase, runtimeState) {
runtimeState := agentCallbackEventRuntimeState(payload.EventType, source)
if upsertAgentCallbackAgentInstance(&record, payload, phase, runtimeState) {
changed = true
}
}
if !changed {
return record, false
}
record.UpdatedAt = firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agnetNow())
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
if err := updateAgnetDeploymentRecord(record); err != nil {
common.SysLog("applyAgnetCallbackDeploymentState: " + err.Error())
record.UpdatedAt = firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agentNow())
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
if err := updateAgentDeploymentRecord(record); err != nil {
common.SysLog("applyAgentCallbackDeploymentState: " + err.Error())
}
return record, true
}
func normalizeCallbackArtifact(payload *agnetCallbackEnvelope) {
func normalizeCallbackArtifact(payload *agentCallbackEnvelope) {
if payload == nil || strings.TrimSpace(payload.Artifact.ArtifactID) != "" {
return
}
@@ -371,7 +371,7 @@ func normalizeCallbackArtifact(payload *agnetCallbackEnvelope) {
if len(source) == 0 {
return
}
payload.Artifact = agnetArtifactPayload{
payload.Artifact = agentArtifactPayload{
ArtifactID: callbackStringValue(source, "artifact_id"),
ArtifactType: callbackStringValue(source, "artifact_type"),
Title: callbackStringValue(source, "title"),
@@ -399,7 +399,7 @@ func normalizeCallbackArtifact(payload *agnetCallbackEnvelope) {
}
}
func persistAgnetArtifactFromCallback(payload agnetCallbackEnvelope, record agnetDeploymentRecord) error {
func persistAgentArtifactFromCallback(payload agentCallbackEnvelope, record agentDeploymentRecord) error {
artifact := payload.Artifact
if strings.TrimSpace(artifact.ArtifactID) == "" {
return nil
@@ -410,7 +410,7 @@ func persistAgnetArtifactFromCallback(payload agnetCallbackEnvelope, record agne
metadataJSON = string(data)
}
}
return model.UpsertAgnetArtifact(&model.AgnetArtifact{
return model.UpsertAgentArtifact(&model.AgentArtifact{
ArtifactID: strings.TrimSpace(artifact.ArtifactID),
DeploymentID: strings.TrimSpace(payload.DeploymentID),
TaskID: strings.TrimSpace(payload.TaskID),
@@ -460,7 +460,7 @@ func callbackIntValue(values map[string]any, key string) int {
}
}
var agnetCallbackEventRequiredFields = map[string][]string{
var agentCallbackEventRequiredFields = map[string][]string{
"deployment.status_changed": {"status"},
"phase.changed": {"stage", "checkpoint"},
"agent.started": {"agent_role"},
@@ -486,7 +486,7 @@ var agnetCallbackEventRequiredFields = map[string][]string{
"budget.alert": {"threshold_pct"},
}
var agnetCallbackEventCategories = map[string]string{
var agentCallbackEventCategories = map[string]string{
"deployment.status_changed": "deployment",
"phase.changed": "ordinary_sub",
"agent.started": "ordinary_sub",
@@ -512,33 +512,33 @@ var agnetCallbackEventCategories = map[string]string{
"budget.alert": "budget",
}
func AgnetGetSwarmEventCallbackSchema(c *gin.Context) {
events := make([]string, 0, len(agnetCallbackEventRequiredFields))
for eventType := range agnetCallbackEventRequiredFields {
func AgentGetRuntimeEventCallbackSchema(c *gin.Context) {
events := make([]string, 0, len(agentCallbackEventRequiredFields))
for eventType := range agentCallbackEventRequiredFields {
events = append(events, eventType)
}
sort.Strings(events)
items := make([]gin.H, 0, len(events))
for _, eventType := range events {
required := append([]string(nil), agnetCallbackEventRequiredFields[eventType]...)
required := append([]string(nil), agentCallbackEventRequiredFields[eventType]...)
sort.Strings(required)
items = append(items, gin.H{
"event_type": eventType,
"category": agnetCallbackEventCategories[eventType],
"category": agentCallbackEventCategories[eventType],
"required_fields": required,
"payload_location": "top-level envelope or payload object; artifact_id may also be in artifact object",
})
}
common.ApiSuccess(c, gin.H{
"callback_path": "/api/agnet/callbacks/swarm-events",
"callback_path": "/api/agent/callbacks/runtime-events",
"auth": gin.H{
"service_token_headers": []string{"X-Agnet-Service-Token", "Authorization: Bearer <token>"},
"hmac_headers": []string{"X-Agnet-Event-Id", "X-Agnet-Timestamp", "X-Agnet-Signature"},
"service_token_headers": []string{"X-Agent-Service-Token", "Authorization: Bearer <token>"},
"hmac_headers": []string{"X-Agent-Event-Id", "X-Agent-Timestamp", "X-Agent-Signature"},
"hmac_payload": "timestamp + \".\" + event_id + \".\" + raw_body",
},
"dedupe_keys": []string{"X-Agnet-Event-Id", "event_id", "idempotency_key"},
"dedupe_keys": []string{"X-Agent-Event-Id", "event_id", "idempotency_key"},
"events": items,
"security": gin.H{
"plaintext_secrets_allowed": false,
@@ -547,7 +547,7 @@ func AgnetGetSwarmEventCallbackSchema(c *gin.Context) {
})
}
func callbackEnvelopeFieldValue(payload agnetCallbackEnvelope, key string) string {
func callbackEnvelopeFieldValue(payload agentCallbackEnvelope, key string) string {
switch key {
case "task_id":
return strings.TrimSpace(payload.TaskID)
@@ -558,7 +558,7 @@ func callbackEnvelopeFieldValue(payload agnetCallbackEnvelope, key string) strin
}
}
func callbackHasFieldValue(payload agnetCallbackEnvelope, key string) bool {
func callbackHasFieldValue(payload agentCallbackEnvelope, key string) bool {
if callbackEnvelopeFieldValue(payload, key) != "" {
return true
}
@@ -578,8 +578,8 @@ func callbackHasFieldValue(payload agnetCallbackEnvelope, key string) bool {
return false
}
func validateAgnetCallbackEventSchema(payload agnetCallbackEnvelope) error {
required, ok := agnetCallbackEventRequiredFields[payload.EventType]
func validateAgentCallbackEventSchema(payload agentCallbackEnvelope) error {
required, ok := agentCallbackEventRequiredFields[payload.EventType]
if !ok {
return nil
}
@@ -591,7 +591,7 @@ func validateAgnetCallbackEventSchema(payload agnetCallbackEnvelope) error {
return nil
}
func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agnetDeploymentRecord) error {
func persistAgentApprovalFromCallback(payload agentCallbackEnvelope, record agentDeploymentRecord) error {
if payload.EventType != "approval.requested" || model.DB == nil {
return nil
}
@@ -601,7 +601,7 @@ func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agne
}
approvalID := firstNonEmpty(callbackStringValue(source, "approval_id"), "appr_"+common.GetUUID())
userID, _ := strconv.Atoi(strings.TrimSpace(record.Plan.UserContext.UserID))
approval := model.AgnetApprovalRequest{
approval := model.AgentApprovalRequest{
ApprovalID: approvalID,
UserId: userID,
DeploymentID: strings.TrimSpace(payload.DeploymentID),
@@ -614,16 +614,16 @@ func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agne
RiskLevel: firstNonEmpty(callbackStringValue(source, "risk_level"), "high"),
RequiresCredential: callbackBoolValue(source, "requires_credential"),
SecretRef: strings.TrimSpace(callbackStringValue(source, "secret_ref")),
Status: agnetApprovalStatusPending,
RequestedBy: firstNonEmpty(callbackStringValue(source, "requested_by"), "agnet-runtime"),
Status: agentApprovalStatusPending,
RequestedBy: firstNonEmpty(callbackStringValue(source, "requested_by"), "agent-runtime"),
RequestReason: firstNonEmpty(callbackStringValue(source, "reason"), callbackStringValue(source, "summary"), "Runtime requested approval"),
TTLSeconds: callbackIntValue(source, "ttl_seconds"),
}
if approval.TTLSeconds <= 0 {
approval.TTLSeconds = defaultAgnetApprovalTTLSeconds
approval.TTLSeconds = defaultAgentApprovalTTLSeconds
}
if approval.TTLSeconds > maxAgnetApprovalTTLSeconds {
approval.TTLSeconds = maxAgnetApprovalTTLSeconds
if approval.TTLSeconds > maxAgentApprovalTTLSeconds {
approval.TTLSeconds = maxAgentApprovalTTLSeconds
}
if approval.SecretRef != "" && !strings.HasPrefix(approval.SecretRef, "azkv://") {
return fmt.Errorf("approval secret_ref must use azkv:// Azure Key Vault reference")
@@ -634,59 +634,59 @@ func persistAgnetApprovalFromCallback(payload agnetCallbackEnvelope, record agne
now := time.Now().UnixMilli()
approval.ExpiresAt = now + int64(approval.TTLSeconds)*1000
var existing model.AgnetApprovalRequest
var existing model.AgentApprovalRequest
if err := model.DB.Where("approval_id = ?", approval.ApprovalID).First(&existing).Error; err == nil {
return nil
}
if err := model.DB.Create(&approval).Error; err != nil {
return err
}
recordAgnetApprovalAudit("approval.requested", &approval, nil, "ok", "")
recordAgentApprovalAudit("approval.requested", &approval, nil, "ok", "")
return nil
}
func AgnetReceiveSwarmEventCallback(c *gin.Context) {
func AgentReceiveRuntimeEventCallback(c *gin.Context) {
rawBody, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20))
if err != nil {
agnetError(c, "CALLBACK_INVALID", "failed to read callback body")
agentError(c, "CALLBACK_INVALID", "failed to read callback body")
return
}
var payload agnetCallbackEnvelope
var payload agentCallbackEnvelope
if err := common.Unmarshal(rawBody, &payload); err != nil {
agnetError(c, "CALLBACK_INVALID", err.Error())
agentError(c, "CALLBACK_INVALID", err.Error())
return
}
if payload.EventID == "" {
payload.EventID = strings.TrimSpace(c.GetHeader("X-Agnet-Event-Id"))
payload.EventID = strings.TrimSpace(c.GetHeader("X-Agent-Event-Id"))
}
if payload.CorrelationID == "" {
payload.CorrelationID = strings.TrimSpace(c.GetHeader("X-Correlation-ID"))
}
payload.EventID = strings.TrimSpace(payload.EventID)
payload.EventType = strings.TrimSpace(payload.EventType)
if !validateAgnetCallbackAuth(c, rawBody, payload.EventID) {
if !validateAgentCallbackAuth(c, rawBody, payload.EventID) {
return
}
if payload.EventID == "" || payload.EventType == "" {
agnetError(c, "CALLBACK_INVALID", "event_id and event_type are required")
agentError(c, "CALLBACK_INVALID", "event_id and event_type are required")
return
}
normalizeCallbackArtifact(&payload)
if payload.IdempotencyKey == "" {
payload.IdempotencyKey = payload.EventID
}
if err := validateAgnetCallbackEventSchema(payload); err != nil {
agnetError(c, "CALLBACK_SCHEMA_INVALID", err.Error())
if err := validateAgentCallbackEventSchema(payload); err != nil {
agentError(c, "CALLBACK_SCHEMA_INVALID", err.Error())
return
}
if agnetCallbackHasPlaintextSecret(payload) {
agnetError(c, "CALLBACK_SECRET_REJECTED", "callbacks must not contain plaintext credential fields")
if agentCallbackHasPlaintextSecret(payload) {
agentError(c, "CALLBACK_SECRET_REJECTED", "callbacks must not contain plaintext credential fields")
return
}
incomingDeploymentID := strings.TrimSpace(payload.DeploymentID)
incomingSwarmID := strings.TrimSpace(payload.SwarmID)
record, _ := agnetCallbackDeploymentContext(incomingDeploymentID, incomingSwarmID)
record, _ := agentCallbackDeploymentContext(incomingDeploymentID, incomingSwarmID)
if strings.TrimSpace(record.DeploymentID) != "" {
payload.DeploymentID = record.DeploymentID
if strings.TrimSpace(payload.SwarmID) == "" {
@@ -697,7 +697,7 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
payload.CorrelationID = record.Plan.Metadata.CorrelationID
}
payloadJSON, _ := common.Marshal(payload)
inserted, err := model.InsertAgnetCallbackEvent(&model.AgnetCallbackEvent{
inserted, err := model.InsertAgentCallbackEvent(&model.AgentCallbackEvent{
EventID: payload.EventID,
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
CallbackType: "swarm-event",
@@ -716,23 +716,23 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
CreatedAtMs: time.Now().UnixMilli(),
})
if err != nil {
common.SysLog("AgnetReceiveSwarmEventCallback: " + err.Error())
agnetError(c, "CALLBACK_PERSIST_FAILED", "failed to persist callback")
common.SysLog("AgentReceiveRuntimeEventCallback: " + err.Error())
agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist callback")
return
}
if inserted {
if err := persistAgnetArtifactFromCallback(payload, record); err != nil {
common.SysLog("persistAgnetArtifactFromCallback: " + err.Error())
agnetError(c, "CALLBACK_PERSIST_FAILED", "failed to persist artifact")
if err := persistAgentArtifactFromCallback(payload, record); err != nil {
common.SysLog("persistAgentArtifactFromCallback: " + err.Error())
agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist artifact")
return
}
if err := persistAgnetApprovalFromCallback(payload, record); err != nil {
common.SysLog("persistAgnetApprovalFromCallback: " + err.Error())
agnetError(c, "CALLBACK_PERSIST_FAILED", "failed to persist approval request")
if err := persistAgentApprovalFromCallback(payload, record); err != nil {
common.SysLog("persistAgentApprovalFromCallback: " + err.Error())
agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist approval request")
return
}
record, _ = applyAgnetCallbackDeploymentState(payload, record)
recordAgnetAuditEvent(agnetEvent{
record, _ = applyAgentCallbackDeploymentState(payload, record)
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "callback." + payload.EventType,
SchemaVersion: 1,
@@ -741,8 +741,8 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
BindingScope: firstPlanBindingScope(record.Plan),
DeploymentID: strings.TrimSpace(payload.DeploymentID),
CorrelationID: strings.TrimSpace(payload.CorrelationID),
OccurredAt: firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agnetNow()),
}, "agnet_callback", strings.TrimSpace(payload.DeploymentID), agnetRequestID(c), "ok")
OccurredAt: firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agentNow()),
}, "agent_callback", strings.TrimSpace(payload.DeploymentID), agentRequestID(c), "ok")
}
common.ApiSuccess(c, gin.H{
"event_id": payload.EventID,
@@ -753,54 +753,54 @@ func AgnetReceiveSwarmEventCallback(c *gin.Context) {
})
}
func AgnetListUserDeploymentArtifacts(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentListUserDeploymentArtifacts(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
items, err := model.ListAgnetArtifacts(model.ListAgnetArtifactsFilter{
items, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
if err != nil {
common.SysLog("AgnetListUserDeploymentArtifacts: " + err.Error())
agnetError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifacts")
common.SysLog("AgentListUserDeploymentArtifacts: " + err.Error())
agentError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifacts")
return
}
common.ApiSuccess(c, gin.H{"deployment_id": record.DeploymentID, "artifacts": items, "items": items, "total": len(items)})
}
func AgnetGetUserDeploymentArtifactContent(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentGetUserDeploymentArtifactContent(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
artifactID := strings.TrimSpace(c.Param("artifact_id"))
if artifactID == "" {
agnetError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
return
}
artifact, found, err := model.GetAgnetArtifactByDeployment(record.DeploymentID, artifactID)
artifact, found, err := model.GetAgentArtifactByDeployment(record.DeploymentID, artifactID)
if err != nil {
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
agnetError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifact")
common.SysLog("AgentGetUserDeploymentArtifactContent: " + err.Error())
agentError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifact")
return
}
if !found {
agnetError(c, "ARTIFACT_NOT_FOUND", "artifact not found")
agentError(c, "ARTIFACT_NOT_FOUND", "artifact not found")
return
}
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
agnetError(c, "RUNTIME_NOT_CONFIGURED", "runtime is not configured")
agentError(c, "RUNTIME_NOT_CONFIGURED", "runtime is not configured")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
resp, err := callAgnetRuntimeArtifactContent(ctx, cfg, record, artifact.ArtifactID)
resp, err := callAgentRuntimeArtifactContent(ctx, cfg, record, artifact.ArtifactID)
if err != nil {
common.SysLog("AgnetGetUserDeploymentArtifactContent: " + err.Error())
agnetError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content")
common.SysLog("AgentGetUserDeploymentArtifactContent: " + err.Error())
agentError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content")
return
}
defer resp.Body.Close()
@@ -818,8 +818,8 @@ func AgnetGetUserDeploymentArtifactContent(c *gin.Context) {
c.DataFromReader(http.StatusOK, resp.ContentLength, contentType, resp.Body, headers)
}
func callbackPayloadMap(row model.AgnetCallbackEvent) map[string]any {
var payload agnetCallbackEnvelope
func callbackPayloadMap(row model.AgentCallbackEvent) map[string]any {
var payload agentCallbackEnvelope
if err := common.UnmarshalJsonStr(row.PayloadJSON, &payload); err != nil {
return nil
}
@@ -829,7 +829,7 @@ func callbackPayloadMap(row model.AgnetCallbackEvent) map[string]any {
return nil
}
func timelineEntryFromCallback(callback model.AgnetCallbackEvent) gin.H {
func timelineEntryFromCallback(callback model.AgentCallbackEvent) gin.H {
payload := callbackPayloadMap(callback)
entry := gin.H{
"kind": "callback",
@@ -852,29 +852,29 @@ func timelineEntryFromCallback(callback model.AgnetCallbackEvent) gin.H {
return entry
}
func AgnetGetUserDeploymentTimeline(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentGetUserDeploymentTimeline(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
events, err := model.ListAgnetAuditEventsByDeployment(record.DeploymentID)
events, err := model.ListAgentAuditEventsByDeployment(record.DeploymentID)
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query audit events")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query audit events")
return
}
callbacks, err := model.ListAgnetCallbackEvents(model.ListAgnetCallbackEventsFilter{DeploymentID: record.DeploymentID, Limit: 500})
callbacks, err := model.ListAgentCallbackEvents(model.ListAgentCallbackEventsFilter{DeploymentID: record.DeploymentID, Limit: 500})
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query callbacks")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query callbacks")
return
}
artifacts, err := model.ListAgnetArtifacts(model.ListAgnetArtifactsFilter{DeploymentID: record.DeploymentID, Limit: 500})
artifacts, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{DeploymentID: record.DeploymentID, Limit: 500})
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query artifacts")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query artifacts")
return
}
snapshots, err := model.ListAgnetSKSnapshots(record.DeploymentID)
snapshots, err := model.ListAgentSKSnapshots(record.DeploymentID)
if err != nil {
agnetError(c, "TIMELINE_QUERY_FAILED", "failed to query sk snapshots")
agentError(c, "TIMELINE_QUERY_FAILED", "failed to query sk snapshots")
return
}
timeline := make([]gin.H, 0, len(events)+len(callbacks)+len(artifacts)+len(snapshots))
@@ -892,7 +892,7 @@ func AgnetGetUserDeploymentTimeline(c *gin.Context) {
}
common.ApiSuccess(c, gin.H{
"deployment_id": record.DeploymentID,
"deployment": record,
"deployment": withDisplayStatus(record),
"events": events,
"callbacks": callbacks,
"artifacts": artifacts,
@@ -132,21 +132,21 @@ func TestValueLooksLikeSecretAndPlaintextScan(t *testing.T) {
}
// P5: a single source of truth for the default model; no placeholder fallback.
func TestDefaultAgnetModelID_SingleSourceNoPlaceholder(t *testing.T) {
def := defaultAgnetModelID()
func TestDefaultAgentModelID_SingleSourceNoPlaceholder(t *testing.T) {
def := defaultAgentModelID()
require.Equal(t, "gpt-5.4", def)
// Draft builder must use the single default, never agnet-model-<role>.
plan := buildAgnetDraftAgentPlan("backend", "", nil)
// Draft builder must use the single default, never agent-model-<role>.
plan := buildAgentDraftAgentPlan("backend", "", nil)
require.Equal(t, def, plan.DefaultModelID)
require.NotContains(t, plan.DefaultModelID, "agnet-model-")
require.NotContains(t, plan.DefaultModelID, "agent-model-")
// Explicit client model is still honored.
plan = buildAgnetDraftAgentPlan("backend", "gpt-5.4-mini", nil)
plan = buildAgentDraftAgentPlan("backend", "gpt-5.4-mini", nil)
require.Equal(t, "gpt-5.4-mini", plan.DefaultModelID)
// Every role template resolves to the single default, no claude-* hardcoding.
for _, tpl := range agnetRoleTemplates() {
for _, tpl := range agentRoleTemplates() {
require.Equal(t, def, tpl.DefaultModel, "role %s", tpl.Key)
}
}
@@ -5,7 +5,7 @@ import (
"github.com/heicode/manager/common"
)
// AgnetRoleTemplate is the platform-recommended role catalog Manager
// AgentRoleTemplate is the platform-recommended role catalog Manager
// surfaces to users when they assemble an AI development team. The
// six canonical roles come from docs/product-package/13-platform-
// description.md §3 and 04-platform-usage-guide.md §第五步.
@@ -14,7 +14,7 @@ import (
// - Roles are platform-defined contracts, not user-editable data.
// Treating them like rows would invite drift between deployments.
// - Permission hints below are *recommendations* the UI uses to
// pre-fill the "what can this Agnet do" confirmation card —
// pre-fill the "what can this Agent do" confirmation card —
// the actual permission grant still goes through ResourceGrant.
// - If we ever need per-tenant role customization, we add an
// overlay table; the canonical set still lives here as the
@@ -24,7 +24,7 @@ import (
// across persistence and the client picker. Display strings can
// be translated, but the key must NEVER change without a coordinated
// frontend rollout.
type AgnetRoleTemplate struct {
type AgentRoleTemplate struct {
Key string `json:"key"`
DisplayName string `json:"display_name"`
Summary string `json:"summary"`
@@ -33,132 +33,132 @@ type AgnetRoleTemplate struct {
RiskLevel string `json:"risk_level"`
}
// defaultAgnetModelID is the single source of truth for the default sub-agent
// defaultAgentModelID is the single source of truth for the default sub-agent
// model. It is aligned to the production-verified NewAPI model and overridable
// via AGNET_DEFAULT_MODEL_ID, so role templates, deployment drafts and runtime
// agent refs never fall back to placeholder names (e.g. agnet-model-<role>)
// via AGENT_DEFAULT_MODEL_ID, so role templates, deployment drafts and runtime
// agent refs never fall back to placeholder names (e.g. agent-model-<role>)
// that production NewAPI cannot route ("No available channel for model ...").
func defaultAgnetModelID() string {
return common.GetEnvOrDefaultString("AGNET_DEFAULT_MODEL_ID", "gpt-5.4")
func defaultAgentModelID() string {
return common.GetEnvOrDefaultString("AGENT_DEFAULT_MODEL_ID", "gpt-5.4")
}
// agnetRoleTemplates returns the canonical six-role catalog. Order
// agentRoleTemplates returns the canonical six-role catalog. Order
// matches the typical lifecycle a user walks through when assembling
// a team: discover -> design -> build -> review -> operate.
//
// Permission hints use the verbs from docs §13.3.4 (Resource Grant)
// and stay deliberately broad — concrete grants come from the user
// resource-binding flow.
func agnetRoleTemplates() []AgnetRoleTemplate {
return []AgnetRoleTemplate{
func agentRoleTemplates() []AgentRoleTemplate {
return []AgentRoleTemplate{
{
Key: "product",
DisplayName: "Product Agnet",
DisplayName: "Product Agent",
Summary: "Refines the user idea into product scope, requirements and acceptance criteria.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:project_docs",
"write:product_spec",
},
RiskLevel: agnetRiskLow,
RiskLevel: agentRiskLow,
},
{
Key: "architect",
DisplayName: "Architect Agnet",
DisplayName: "Architect Agent",
Summary: "Designs the technical approach, picks frameworks, and breaks work into sub-tasks.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"write:architecture_doc",
},
RiskLevel: agnetRiskLow,
RiskLevel: agentRiskLow,
},
{
Key: "frontend",
DisplayName: "Frontend Agnet",
DisplayName: "Frontend Agent",
Summary: "Implements UI, components and client-side state per the architect's plan.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"write:repo:frontend",
},
RiskLevel: agnetRiskMedium,
RiskLevel: agentRiskMedium,
},
{
Key: "backend",
DisplayName: "Backend Agnet",
DisplayName: "Backend Agent",
Summary: "Implements server-side APIs, data models and integrations.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"write:repo:backend",
"read:dev_database",
},
RiskLevel: agnetRiskMedium,
RiskLevel: agentRiskMedium,
},
{
Key: "reviewer",
DisplayName: "Reviewer Agnet",
DisplayName: "Reviewer Agent",
Summary: "Performs code review, security checks and runs the test suite.",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"run:tests",
"comment:pull_request",
},
RiskLevel: agnetRiskLow,
RiskLevel: agentRiskLow,
},
{
Key: "ops",
DisplayName: "Ops Agnet",
DisplayName: "Ops Agent",
Summary: "Deploys to test environments, watches logs and prepares production rollouts (production requires approval).",
DefaultModel: defaultAgnetModelID(),
DefaultModel: defaultAgentModelID(),
DefaultPermissions: []string{
"read:repo",
"deploy:test_env",
"read:metrics",
"approval_required:deploy_prod",
},
RiskLevel: agnetRiskHigh,
RiskLevel: agentRiskHigh,
},
}
}
// AgnetListRoleTemplates is the GET /api/agnet/role-templates handler.
// AgentListRoleTemplates is the GET /api/agent/role-templates handler.
// Returns the canonical six-role catalog so the deployment-creation
// UI can pre-populate role pickers and the documentation page can
// render the role overview.
//
// Auth: requires UserAuth (mounted by router). Anyone logged in to
// Manager can read the catalog; there are no secrets in the payload.
func AgnetListRoleTemplates(c *gin.Context) {
tpls := agnetRoleTemplates()
func AgentListRoleTemplates(c *gin.Context) {
tpls := agentRoleTemplates()
common.ApiSuccess(c, gin.H{
"items": tpls,
"total": len(tpls),
})
}
// agnetRoleTemplateKeys is a helper for validation in deployment
// agentRoleTemplateKeys is a helper for validation in deployment
// creation — checks whether a user-provided role_template string is
// one of the canonical six. Returns true for any of the canonical
// keys; returns true for unknown keys too (deployment flow today
// accepts free-form role_template strings, see agnet_control_plane.
// accepts free-form role_template strings, see agent_control_plane.
// go:552), so this helper is currently advisory. When we tighten
// validation (after frontend ships the new picker), flip the
// fallback to false and add a unit test.
func agnetRoleTemplateKeys() map[string]bool {
func agentRoleTemplateKeys() map[string]bool {
keys := make(map[string]bool)
for _, t := range agnetRoleTemplates() {
for _, t := range agentRoleTemplates() {
keys[t.Key] = true
}
return keys
}
// agnetIsCanonicalRoleKey reports whether `key` matches one of the
// agentIsCanonicalRoleKey reports whether `key` matches one of the
// six platform-defined roles. Today the deployment endpoint accepts
// any non-empty string; this helper is reserved for the next step
// when we move to a closed set.
func agnetIsCanonicalRoleKey(key string) bool {
return agnetRoleTemplateKeys()[key]
func agentIsCanonicalRoleKey(key string) bool {
return agentRoleTemplateKeys()[key]
}
@@ -9,13 +9,13 @@ import (
"github.com/heicode/manager/common"
)
func TestAgnetRoleTemplates_CanonicalSetCovered(t *testing.T) {
func TestAgentRoleTemplates_CanonicalSetCovered(t *testing.T) {
// Pins the six canonical role keys from docs §13.3.3. Any code
// change that adds, removes or renames a key MUST update this
// list — guards against accidental drift between Manager and
// the product spec.
want := []string{"product", "architect", "frontend", "backend", "reviewer", "ops"}
got := agnetRoleTemplates()
got := agentRoleTemplates()
if len(got) != len(want) {
t.Fatalf("expected %d roles, got %d", len(want), len(got))
}
@@ -37,7 +37,7 @@ func TestAgnetRoleTemplates_CanonicalSetCovered(t *testing.T) {
}
}
func TestAgnetRoleTemplates_RiskLevels(t *testing.T) {
func TestAgentRoleTemplates_RiskLevels(t *testing.T) {
// Ops is the only canonical role with high risk (production
// deployment intent). Reviewer + Product + Architect stay low
// (read-mostly). Frontend + Backend land at medium. Pins the
@@ -45,29 +45,29 @@ func TestAgnetRoleTemplates_RiskLevels(t *testing.T) {
// flip an ops role to "low" and skip the high-risk approval
// gating downstream.
wantRisk := map[string]string{
"product": agnetRiskLow,
"architect": agnetRiskLow,
"reviewer": agnetRiskLow,
"frontend": agnetRiskMedium,
"backend": agnetRiskMedium,
"ops": agnetRiskHigh,
"product": agentRiskLow,
"architect": agentRiskLow,
"reviewer": agentRiskLow,
"frontend": agentRiskMedium,
"backend": agentRiskMedium,
"ops": agentRiskHigh,
}
for _, tpl := range agnetRoleTemplates() {
for _, tpl := range agentRoleTemplates() {
if want, ok := wantRisk[tpl.Key]; ok && tpl.RiskLevel != want {
t.Errorf("role %q: want risk %q, got %q", tpl.Key, want, tpl.RiskLevel)
}
}
}
func TestAgnetListRoleTemplates_HTTPShape(t *testing.T) {
func TestAgentListRoleTemplates_HTTPShape(t *testing.T) {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest("GET", "/api/agnet/role-templates", nil)
c.Request = httptest.NewRequest("GET", "/api/agent/role-templates", nil)
c.Set("id", 1)
c.Set("role", common.RoleCommonUser)
AgnetListRoleTemplates(c)
AgentListRoleTemplates(c)
if rec.Code != 200 {
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
@@ -83,17 +83,17 @@ func TestAgnetListRoleTemplates_HTTPShape(t *testing.T) {
}
}
func TestAgnetIsCanonicalRoleKey(t *testing.T) {
func TestAgentIsCanonicalRoleKey(t *testing.T) {
// Defensive helper currently used as advisory — pins the closed
// set so the future tighten-up to closed-set validation is one
// flip instead of an open-ended audit.
for _, ok := range []string{"product", "architect", "frontend", "backend", "reviewer", "ops"} {
if !agnetIsCanonicalRoleKey(ok) {
if !agentIsCanonicalRoleKey(ok) {
t.Errorf("%q should be canonical", ok)
}
}
for _, bad := range []string{"", "debugger", "executor", "random_string"} {
if agnetIsCanonicalRoleKey(bad) {
if agentIsCanonicalRoleKey(bad) {
t.Errorf("%q should NOT be canonical", bad)
}
}
@@ -17,15 +17,15 @@ import (
)
const (
agnetRuntimeStateSyncing = "runtime_syncing"
agnetRuntimeStateSynced = "runtime_accepted"
agnetRuntimeStateFailed = "runtime_sync_failed"
agentRuntimeStateSyncing = "runtime_syncing"
agentRuntimeStateSynced = "runtime_accepted"
agentRuntimeStateFailed = "runtime_sync_failed"
agnetRuntimeModeAgnet = "agnet"
agnetRuntimeModeSwarm = "swarm"
agentRuntimeModeAgent = "agent"
agentRuntimeModeSwarm = "swarm"
)
type agnetRuntimeConfig struct {
type agentRuntimeConfig struct {
Enabled bool
Async bool
BaseURL string
@@ -39,14 +39,14 @@ type agnetRuntimeConfig struct {
Timeout time.Duration
}
type agnetRuntimeSyncResult struct {
type agentRuntimeSyncResult struct {
RuntimeDeploymentID string
RuntimeSwarmID string
RuntimeStatus string
RawStatusCode int
}
type agnetRuntimeDiagnostics struct {
type agentRuntimeDiagnostics struct {
DeploymentID string `json:"deployment_id"`
RuntimeMode string `json:"runtime_mode"`
SubMode string `json:"sub_mode"`
@@ -65,41 +65,42 @@ type agnetRuntimeDiagnostics struct {
CheckedAt string `json:"checked_at"`
}
func normalizeAgnetRuntimeMode(value string) string {
func normalizeAgentRuntimeMode(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case agnetRuntimeModeSwarm:
return agnetRuntimeModeSwarm
case agentRuntimeModeSwarm:
return agentRuntimeModeSwarm
default:
return agnetRuntimeModeAgnet
return agentRuntimeModeAgent
}
}
func agnetRuntimeModeForSource(source string) string {
func agentRuntimeModeForSource(source string) string {
if strings.TrimSpace(source) == "api_swarms_adapter" {
return agnetRuntimeModeSwarm
return agentRuntimeModeSwarm
}
return agnetRuntimeModeAgnet
return agentRuntimeModeAgent
}
func agnetRuntimeModeForRecord(record agnetDeploymentRecord) string {
return normalizeAgnetRuntimeMode(record.Plan.Metadata.RuntimeMode)
func agentRuntimeModeForRecord(record agentDeploymentRecord) string {
return normalizeAgentRuntimeMode(record.Plan.Metadata.RuntimeMode)
}
func agnetRuntimeClientConfig() agnetRuntimeConfig {
return agnetRuntimeClientConfigForMode(agnetRuntimeModeAgnet)
func agentRuntimeClientConfig() agentRuntimeConfig {
return agentRuntimeClientConfigForMode(agentRuntimeModeAgent)
}
func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
timeoutSec := common.GetEnvOrDefault("AGNET_RUNTIME_TIMEOUT_SECONDS", 5)
func agentRuntimeClientConfigForMode(mode string) agentRuntimeConfig {
timeoutSec := common.GetEnvOrDefault("AGENT_RUNTIME_TIMEOUT_SECONDS", 5)
if timeoutSec <= 0 {
timeoutSec = 5
}
mode = normalizeAgnetRuntimeMode(mode)
prefix := "AGNET_RUNTIME_"
defaultCreatePath := "/api/agnet/deployments"
defaultStopPath := "/api/agnet/deployments/{deployment_id}/stop"
mode = normalizeAgentRuntimeMode(mode)
prefix := "AGENT_RUNTIME_"
// Sub Agile primary route per agent_management Sub Mode Runtime §2.2.
defaultCreatePath := "/api/agent/sub-agile/deployments"
defaultStopPath := "/api/agent/sub-agile/deployments/{deployment_id}/stop"
defaultApprovalPath := "/api/swarms/{swarm_id}/approvals/{approval_id}"
if mode == agnetRuntimeModeSwarm {
if mode == agentRuntimeModeSwarm {
prefix = "SWARM_RUNTIME_"
defaultCreatePath = "/api/swarms"
defaultStopPath = "/api/swarms/{swarm_id}/stop"
@@ -110,18 +111,18 @@ func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
}
baseURL := strings.TrimRight(strings.TrimSpace(common.GetEnvOrDefaultString(prefix+"BASE_URL", "")), "/")
enabledDefault := false
if mode == agnetRuntimeModeAgnet {
enabledDefault = common.GetEnvOrDefaultBool("AGNET_RUNTIME_ENABLED", false)
if mode == agentRuntimeModeAgent {
enabledDefault = common.GetEnvOrDefaultBool("AGENT_RUNTIME_ENABLED", false)
} else {
enabledDefault = baseURL != ""
}
return agnetRuntimeConfig{
return agentRuntimeConfig{
Enabled: common.GetEnvOrDefaultBool(prefix+"ENABLED", enabledDefault),
Async: common.GetEnvOrDefaultBool(prefix+"ASYNC", common.GetEnvOrDefaultBool("AGNET_RUNTIME_ASYNC", true)),
Async: common.GetEnvOrDefaultBool(prefix+"ASYNC", common.GetEnvOrDefaultBool("AGENT_RUNTIME_ASYNC", true)),
BaseURL: baseURL,
Token: strings.TrimSpace(common.GetEnvOrDefaultString(prefix+"SERVICE_TOKEN", "")),
CreatePath: common.GetEnvOrDefaultString(prefix+"CREATE_PATH", defaultCreatePath),
HealthPath: common.GetEnvOrDefaultString(prefix+"HEALTH_PATH", "/api/agnet/health"),
HealthPath: common.GetEnvOrDefaultString(prefix+"HEALTH_PATH", "/api/agent/health"),
StatusPath: common.GetEnvOrDefaultString(prefix+"STATUS_PATH", "/api/swarms/{swarm_id}/status"),
ArtifactContentPath: common.GetEnvOrDefaultString(prefix+"ARTIFACT_CONTENT_PATH", "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"),
StopPath: common.GetEnvOrDefaultString(prefix+"STOP_PATH", defaultStopPath),
@@ -130,19 +131,19 @@ func agnetRuntimeClientConfigForMode(mode string) agnetRuntimeConfig {
}
}
func agnetRuntimeCallbackURL() string {
if value := strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_CALLBACK_URL", "")); value != "" {
func agentRuntimeCallbackURL() string {
if value := strings.TrimSpace(common.GetEnvOrDefaultString("AGENT_RUNTIME_CALLBACK_URL", "")); value != "" {
return value
}
baseURL := strings.TrimRight(strings.TrimSpace(common.GetEnvOrDefaultString("HEICODE_PUBLIC_BASE_URL", "https://code.xinghanlab.com")), "/")
return baseURL + "/api/agnet/callbacks/swarm-events"
return baseURL + "/api/agent/callbacks/runtime-events"
}
func agnetRuntimeCallbackSigningSecretRef() string {
return strings.TrimSpace(common.GetEnvOrDefaultString("AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""))
func agentRuntimeCallbackSigningSecretRef() string {
return strings.TrimSpace(common.GetEnvOrDefaultString("AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""))
}
func agnetRuntimeSubscribedEvents() []string {
func agentRuntimeSubscribedEvents() []string {
return []string{
"deployment.status_changed",
"phase.changed",
@@ -178,7 +179,7 @@ func firstNonEmpty(values ...string) string {
return ""
}
func truncateAgnetFailureReason(value string) string {
func truncateAgentFailureReason(value string) string {
value = strings.TrimSpace(value)
if len(value) <= 480 {
return value
@@ -186,13 +187,13 @@ func truncateAgnetFailureReason(value string) string {
return value[:480]
}
func agnetRuntimeURL(baseURL string, path string) (string, error) {
func agentRuntimeURL(baseURL string, path string) (string, error) {
if strings.TrimSpace(baseURL) == "" {
return "", errors.New("AGNET_RUNTIME_BASE_URL is not configured")
return "", errors.New("AGENT_RUNTIME_BASE_URL is not configured")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", errors.New("AGNET_RUNTIME_BASE_URL must be an absolute http(s) URL")
return "", errors.New("AGENT_RUNTIME_BASE_URL must be an absolute http(s) URL")
}
if strings.TrimSpace(path) == "" {
path = "/"
@@ -203,7 +204,7 @@ func agnetRuntimeURL(baseURL string, path string) (string, error) {
return strings.TrimRight(baseURL, "/") + path, nil
}
func agnetRuntimeHeaders(req *http.Request, cfg agnetRuntimeConfig, record agnetDeploymentRecord) {
func agentRuntimeHeaders(req *http.Request, cfg agentRuntimeConfig, record agentDeploymentRecord) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-ID", record.Plan.UserContext.UserID)
req.Header.Set("X-Binding-Scope", firstPlanBindingScope(record.Plan))
@@ -214,8 +215,8 @@ func agnetRuntimeHeaders(req *http.Request, cfg agnetRuntimeConfig, record agnet
}
}
func agnetRuntimeRequestAgents(plan agnetOrchestrationPlan) []gin.H {
byRole := make(map[string]agnetAgentPlan, len(plan.Agents))
func agentRuntimeRequestAgents(plan agentOrchestrationPlan) []gin.H {
byRole := make(map[string]agentAgentPlan, len(plan.Agents))
for _, agent := range plan.Agents {
role := strings.TrimSpace(agent.RoleTemplate)
if role != "" {
@@ -235,7 +236,7 @@ func agnetRuntimeRequestAgents(plan agnetOrchestrationPlan) []gin.H {
item["sk_sources"] = agent.SKSources
}
if len(agent.ResourceGrants) > 0 {
item["resource_grants"] = agnetRuntimeResourceGrantPayloads(agent.ResourceGrants)
item["resource_grants"] = agentRuntimeResourceGrantPayloads(agent.ResourceGrants)
}
}
items = append(items, item)
@@ -254,14 +255,14 @@ func agnetRuntimeRequestAgents(plan agnetOrchestrationPlan) []gin.H {
item["sk_sources"] = agent.SKSources
}
if len(agent.ResourceGrants) > 0 {
item["resource_grants"] = agnetRuntimeResourceGrantPayloads(agent.ResourceGrants)
item["resource_grants"] = agentRuntimeResourceGrantPayloads(agent.ResourceGrants)
}
items = append(items, item)
}
return items
}
func agnetRuntimeRequestSwarmAgents(plan agnetOrchestrationPlan) []gin.H {
func agentRuntimeRequestSwarmAgents(plan agentOrchestrationPlan) []gin.H {
runtimeModels := make(map[string]string, len(plan.AgentRuntime.Agents))
for _, runtimeAgent := range plan.AgentRuntime.Agents {
role := strings.TrimSpace(runtimeAgent.Role)
@@ -276,7 +277,7 @@ func agnetRuntimeRequestSwarmAgents(plan agnetOrchestrationPlan) []gin.H {
if role == "" {
continue
}
taskID := fmt.Sprintf("%s-%d", sanitizeAgnetRef(role), index+1)
taskID := fmt.Sprintf("%s-%d", sanitizeAgentRef(role), index+1)
item := gin.H{
"task_id": taskID,
"role": role,
@@ -291,14 +292,14 @@ func agnetRuntimeRequestSwarmAgents(plan agnetOrchestrationPlan) []gin.H {
item["sk_sources"] = agent.SKSources
}
if len(agent.ResourceGrants) > 0 {
item["resource_grants"] = agnetRuntimeResourceGrantPayloads(agent.ResourceGrants)
item["resource_grants"] = agentRuntimeResourceGrantPayloads(agent.ResourceGrants)
}
items = append(items, item)
}
return items
}
func agnetRuntimeResourceGrantPayloads(grants []agnetResourceGrant) []gin.H {
func agentRuntimeResourceGrantPayloads(grants []agentResourceGrant) []gin.H {
items := make([]gin.H, 0, len(grants))
for _, grant := range grants {
secretRef := strings.TrimSpace(grant.SecretRef)
@@ -319,8 +320,8 @@ func agnetRuntimeResourceGrantPayloads(grants []agnetResourceGrant) []gin.H {
"status": grant.Status,
"ref": secretRef,
"secret_ref": secretRef,
"allowed_ref": agnetGrantResourceRef(grant),
"resource_ref_hint": agnetGrantResourceRef(grant),
"allowed_ref": agentGrantResourceRef(grant),
"resource_ref_hint": agentGrantResourceRef(grant),
}
if grant.TenantID != "" {
item["tenant_id"] = grant.TenantID
@@ -342,23 +343,23 @@ func agnetRuntimeResourceGrantPayloads(grants []agnetResourceGrant) []gin.H {
return items
}
func agnetRuntimeRequestResourceGrants(plan agnetOrchestrationPlan) []gin.H {
items := make([]agnetResourceGrant, 0, len(plan.ResourceGrants))
func agentRuntimeRequestResourceGrants(plan agentOrchestrationPlan) []gin.H {
items := make([]agentResourceGrant, 0, len(plan.ResourceGrants))
items = append(items, plan.ResourceGrants...)
for _, agent := range plan.Agents {
items = append(items, agent.ResourceGrants...)
}
return agnetRuntimeResourceGrantPayloads(items)
return agentRuntimeResourceGrantPayloads(items)
}
func agnetRuntimeRequestMetadata(record agnetDeploymentRecord, source string) gin.H {
func agentRuntimeRequestMetadata(record agentDeploymentRecord, source string) gin.H {
metadata := gin.H{
"correlation_id": record.Plan.Metadata.CorrelationID,
"manager_deployment_id": record.DeploymentID,
"source": source,
"heicode_deployment_id": record.DeploymentID,
"heicode_runtime_bridge": true,
"runtime_mode": agnetRuntimeModeForRecord(record),
"runtime_mode": agentRuntimeModeForRecord(record),
}
if record.Plan.Metadata.TenantID != "" {
metadata["tenant_id"] = record.Plan.Metadata.TenantID
@@ -369,7 +370,7 @@ func agnetRuntimeRequestMetadata(record agnetDeploymentRecord, source string) gi
return metadata
}
func agnetRuntimeBudgetPayload(budget agnetBudget) gin.H {
func agentRuntimeBudgetPayload(budget agentBudget) gin.H {
return gin.H{
"max_tokens": budget.MaxTokens,
"token_limit": budget.MaxTokens,
@@ -380,8 +381,8 @@ func agnetRuntimeBudgetPayload(budget agnetBudget) gin.H {
}
}
func agnetRuntimeOrchestrationPlanPayload(record agnetDeploymentRecord) any {
if agnetRuntimeModeForRecord(record) != agnetRuntimeModeSwarm {
func agentRuntimeOrchestrationPlanPayload(record agentDeploymentRecord) any {
if agentRuntimeModeForRecord(record) != agentRuntimeModeSwarm {
return record.Plan
}
plan := record.Plan
@@ -391,50 +392,50 @@ func agnetRuntimeOrchestrationPlanPayload(record agnetDeploymentRecord) any {
"objective": plan.Objective,
"sub_mode": firstNonEmpty(plan.SubMode, "goal_driven_swarm"),
"risk_level": plan.RiskLevel,
"budget": agnetRuntimeBudgetPayload(plan.Budget),
"budget": agentRuntimeBudgetPayload(plan.Budget),
"user_context": plan.UserContext,
"billing_context": plan.BillingContext,
"agile_context": plan.AgileContext,
"agents": agnetRuntimeRequestSwarmAgents(plan),
"resource_grants": agnetRuntimeRequestResourceGrants(plan),
"agents": agentRuntimeRequestSwarmAgents(plan),
"resource_grants": agentRuntimeRequestResourceGrants(plan),
"constraints": plan.Constraints,
"metadata": agnetRuntimeRequestMetadata(record, "orchestration_plan"),
"metadata": agentRuntimeRequestMetadata(record, "orchestration_plan"),
"agent_runtime": plan.AgentRuntime,
"acceptance": plan.AgileContext.AcceptanceCriteria,
"acceptance_tests": plan.AgileContext.AcceptanceCriteria,
}
}
func agnetRuntimeCreatePayload(record agnetDeploymentRecord, source string) gin.H {
func agentRuntimeCreatePayload(record agentDeploymentRecord, source string) gin.H {
callback := gin.H{
"url": agnetRuntimeCallbackURL(),
"subscribed_events": agnetRuntimeSubscribedEvents(),
"url": agentRuntimeCallbackURL(),
"subscribed_events": agentRuntimeSubscribedEvents(),
}
if ref := agnetRuntimeCallbackSigningSecretRef(); ref != "" {
if ref := agentRuntimeCallbackSigningSecretRef(); ref != "" {
callback["signing_secret_ref"] = ref
}
return gin.H{
"orchestration_plan": agnetRuntimeOrchestrationPlanPayload(record),
"agents": agnetRuntimeRequestAgents(record.Plan),
"orchestration_plan": agentRuntimeOrchestrationPlanPayload(record),
"agents": agentRuntimeRequestAgents(record.Plan),
"risk_level": record.Plan.RiskLevel,
"budget": agnetRuntimeBudgetPayload(record.Plan.Budget),
"budget": agentRuntimeBudgetPayload(record.Plan.Budget),
"billing_context": record.Plan.BillingContext,
"resource_grants": agnetRuntimeRequestResourceGrants(record.Plan),
"resource_grants": agentRuntimeRequestResourceGrants(record.Plan),
"callback": callback,
"agile_context": record.Plan.AgileContext,
"sub_mode": record.Plan.SubMode,
"metadata": agnetRuntimeRequestMetadata(record, source),
"metadata": agentRuntimeRequestMetadata(record, source),
}
}
func extractAgnetRuntimeData(payload map[string]any) map[string]any {
func extractAgentRuntimeData(payload map[string]any) map[string]any {
if data, ok := payload["data"].(map[string]any); ok {
return data
}
return payload
}
func agnetRuntimeEnvelopeError(payload map[string]any) string {
func agentRuntimeEnvelopeError(payload map[string]any) string {
success, hasSuccess := payload["success"].(bool)
if !hasSuccess || success {
return ""
@@ -463,44 +464,44 @@ func stringFromMap(values map[string]any, keys ...string) string {
return ""
}
func callAgnetRuntimeCreate(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, source string) (agnetRuntimeSyncResult, error) {
endpoint, err := agnetRuntimeURL(cfg.BaseURL, cfg.CreatePath)
func callAgentRuntimeCreate(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, source string) (agentRuntimeSyncResult, error) {
endpoint, err := agentRuntimeURL(cfg.BaseURL, cfg.CreatePath)
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
payload, err := common.Marshal(agnetRuntimeCreatePayload(record, source))
payload, err := common.Marshal(agentRuntimeCreatePayload(record, source))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
defer resp.Body.Close()
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if readErr != nil {
return agnetRuntimeSyncResult{}, readErr
return agentRuntimeSyncResult{}, readErr
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime create returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime create returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var envelope map[string]any
if len(body) > 0 {
if err := common.Unmarshal(body, &envelope); err != nil {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
}
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
}
data := extractAgnetRuntimeData(envelope)
result := agnetRuntimeSyncResult{
data := extractAgentRuntimeData(envelope)
result := agentRuntimeSyncResult{
RuntimeDeploymentID: stringFromMap(data, "runtime_deployment_id", "deployment_id", "id"),
RuntimeSwarmID: stringFromMap(data, "swarm_id", "runtime_swarm_id"),
RuntimeStatus: stringFromMap(data, "runtime_status", "status"),
@@ -509,16 +510,18 @@ func callAgnetRuntimeCreate(ctx context.Context, cfg agnetRuntimeConfig, record
return result, nil
}
func agnetRuntimeStopPath(cfg agnetRuntimeConfig, runtimeDeploymentID string) string {
func agentRuntimeStopPath(cfg agentRuntimeConfig, runtimeDeploymentID string) string {
path := strings.TrimSpace(cfg.StopPath)
if path == "" {
path = "/api/agnet/deployments/{deployment_id}/stop"
path = "/api/agent/deployments/{deployment_id}/stop"
}
return strings.ReplaceAll(path, "{deployment_id}", url.PathEscape(runtimeDeploymentID))
}
func agnetRuntimeStopPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord) string {
path := agnetRuntimeStopPath(cfg, strings.TrimSpace(record.RuntimeDeploymentID))
func agentRuntimeStopPathForRecord(cfg agentRuntimeConfig, record agentDeploymentRecord) string {
// {deployment_id} falls back to the swarm id so sub-agile-style paths still
// resolve for records that only persisted a runtime swarm id.
path := agentRuntimeStopPath(cfg, firstNonEmpty(record.RuntimeDeploymentID, record.RuntimeSwarmID))
replacer := strings.NewReplacer(
"{swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
"{runtime_swarm_id}", url.PathEscape(strings.TrimSpace(record.RuntimeSwarmID)),
@@ -528,7 +531,7 @@ func agnetRuntimeStopPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymen
return replacer.Replace(path)
}
func agnetRuntimeStatusPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord) string {
func agentRuntimeStatusPathForRecord(cfg agentRuntimeConfig, record agentDeploymentRecord) string {
path := strings.TrimSpace(cfg.StatusPath)
if path == "" {
path = "/api/swarms/{swarm_id}/status"
@@ -543,7 +546,7 @@ func agnetRuntimeStatusPathForRecord(cfg agnetRuntimeConfig, record agnetDeploym
return replacer.Replace(path)
}
func agnetRuntimeArtifactContentPathForRecord(cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) string {
func agentRuntimeArtifactContentPathForRecord(cfg agentRuntimeConfig, record agentDeploymentRecord, artifactID string) string {
path := strings.TrimSpace(cfg.ArtifactContentPath)
if path == "" {
path = "/api/swarms/{swarm_id}/artifacts/{artifact_id}/content"
@@ -559,11 +562,11 @@ func agnetRuntimeArtifactContentPathForRecord(cfg agnetRuntimeConfig, record agn
return replacer.Replace(path)
}
func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord) (map[string]any, int, error) {
func callAgentRuntimeStatus(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord) (map[string]any, int, error) {
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return nil, 0, errors.New("runtime identifiers missing")
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStatusPathForRecord(cfg, record))
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeStatusPathForRecord(cfg, record))
if err != nil {
return nil, 0, err
}
@@ -571,7 +574,7 @@ func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record
if err != nil {
return nil, 0, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
@@ -591,20 +594,20 @@ func callAgnetRuntimeStatus(ctx context.Context, cfg agnetRuntimeConfig, record
return nil, resp.StatusCode, err
}
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return nil, resp.StatusCode, errors.New(message)
}
return extractAgnetRuntimeData(envelope), resp.StatusCode, nil
return extractAgentRuntimeData(envelope), resp.StatusCode, nil
}
func callAgnetRuntimeArtifactContent(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, artifactID string) (*http.Response, error) {
func callAgentRuntimeArtifactContent(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, artifactID string) (*http.Response, error) {
if strings.TrimSpace(artifactID) == "" {
return nil, errors.New("artifact_id is required")
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return nil, errors.New("runtime identifiers missing")
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeArtifactContentPathForRecord(cfg, record, artifactID))
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeArtifactContentPathForRecord(cfg, record, artifactID))
if err != nil {
return nil, err
}
@@ -612,7 +615,7 @@ func callAgnetRuntimeArtifactContent(ctx context.Context, cfg agnetRuntimeConfig
if err != nil {
return nil, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
req.Header.Set("Accept", "*/*")
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
@@ -719,6 +722,14 @@ func anyPositiveFileSignal(values map[string]any) bool {
// artifact_type="document" (its no-files fallback) under a uri scheme that the
// old "/artifacts/summary" heuristic no longer matched.
func artifactIsSummaryOnly(artifact gin.H) bool {
// Runtime marks fallback artifacts (no real agent output) with
// metadata.synthesized=true — the authoritative non-deliverable signal
// (agent_management Sub Mode Runtime §7.2).
if meta, ok := artifact["metadata"].(map[string]any); ok {
if synth, ok := meta["synthesized"].(bool); ok && synth {
return true
}
}
atype := strings.ToLower(strings.TrimSpace(fmt.Sprint(artifact["artifact_type"])))
if deliverableArtifactTypes[atype] || artifactHasFileChanges(artifact) {
return false
@@ -749,7 +760,61 @@ func runtimeArtifactsAreSummaryOnly(artifacts []gin.H) bool {
return true
}
func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]any, httpStatus int, source string) agnetRuntimeDiagnostics {
// persistedArtifactToGin adapts a stored artifact to the gin.H shape consumed
// by artifactIsSummaryOnly.
func persistedArtifactToGin(a model.AgentArtifact) gin.H {
g := gin.H{
"artifact_id": a.ArtifactID,
"artifact_type": a.ArtifactType,
"title": a.Title,
"summary": a.Summary,
"uri": a.URI,
}
if strings.TrimSpace(a.MetadataJSON) != "" {
var meta map[string]any
if err := common.UnmarshalJsonStr(a.MetadataJSON, &meta); err == nil && len(meta) > 0 {
g["metadata"] = meta
}
}
return g
}
// agentDeploymentDisplayStatus is the single status the client should show.
// Manager is the sole judge (unified spec §10.6): a `completed` runtime status
// is only surfaced as `completed` when there is a real (non-summary)
// deliverable; otherwise it is downgraded so an empty result is not shown as
// success — `needs_codegen` when only a plan/summary exists, or
// `completed_without_deliverable` when no artifact exists at all.
func agentDeploymentDisplayStatus(record agentDeploymentRecord) string {
if strings.ToLower(strings.TrimSpace(record.Status)) != "completed" {
return record.Status
}
artifacts, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
if err != nil {
common.SysLog("agentDeploymentDisplayStatus: " + err.Error())
return record.Status
}
for _, a := range artifacts {
if !artifactIsSummaryOnly(persistedArtifactToGin(a)) {
return "completed"
}
}
if len(artifacts) > 0 {
return "needs_codegen"
}
return "completed_without_deliverable"
}
// withDisplayStatus returns the record with DisplayStatus computed for response.
func withDisplayStatus(record agentDeploymentRecord) agentDeploymentRecord {
record.DisplayStatus = agentDeploymentDisplayStatus(record)
return record
}
func buildAgentRuntimeDiagnostics(record agentDeploymentRecord, data map[string]any, httpStatus int, source string) agentRuntimeDiagnostics {
agents := mapSliceFromAny(data["agents"])
artifacts := mapSliceFromAny(data["artifacts"])
status := stringFromMap(data, "runtime_status", "status")
@@ -771,9 +836,9 @@ func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]
warnings = append(warnings, "runtime_zero_model_usage")
}
}
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: agnetRuntimeModeForRecord(record),
RuntimeMode: agentRuntimeModeForRecord(record),
SubMode: record.SubMode,
RuntimeDeploymentID: record.RuntimeDeploymentID,
RuntimeSwarmID: record.RuntimeSwarmID,
@@ -787,36 +852,36 @@ func buildAgnetRuntimeDiagnostics(record agnetDeploymentRecord, data map[string]
Artifacts: artifacts,
Metrics: metrics,
Warnings: warnings,
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
func agnetRuntimeDiagnosticsForRecord(ctx context.Context, record agnetDeploymentRecord) agnetRuntimeDiagnostics {
mode := agnetRuntimeModeForRecord(record)
cfg := agnetRuntimeClientConfigForMode(mode)
func agentRuntimeDiagnosticsForRecord(ctx context.Context, record agentDeploymentRecord) agentRuntimeDiagnostics {
mode := agentRuntimeModeForRecord(record)
cfg := agentRuntimeClientConfigForMode(mode)
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
DataSource: "not_configured",
Warnings: []string{"runtime_not_configured"},
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
DataSource: "missing_runtime_id",
Warnings: []string{"runtime_identifiers_missing"},
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
data, status, err := callAgnetRuntimeStatus(ctx, cfg, record)
data, status, err := callAgentRuntimeStatus(ctx, cfg, record)
if err != nil {
return agnetRuntimeDiagnostics{
return agentRuntimeDiagnostics{
DeploymentID: record.DeploymentID,
RuntimeMode: mode,
SubMode: record.SubMode,
@@ -824,25 +889,25 @@ func agnetRuntimeDiagnosticsForRecord(ctx context.Context, record agnetDeploymen
RuntimeSwarmID: record.RuntimeSwarmID,
DataSource: "runtime_status_error",
HTTPStatus: status,
ErrorMessage: truncateAgnetFailureReason(err.Error()),
ErrorMessage: truncateAgentFailureReason(err.Error()),
Warnings: []string{"runtime_status_query_failed"},
CheckedAt: agnetNow(),
CheckedAt: agentNow(),
}
}
return buildAgnetRuntimeDiagnostics(record, data, status, "runtime_status")
return buildAgentRuntimeDiagnostics(record, data, status, "runtime_status")
}
func AgnetGetUserDeploymentRuntimeDiagnostics(c *gin.Context) {
record, ok := requireAuthenticatedUserAgnetDeployment(c)
func AgentGetUserDeploymentRuntimeDiagnostics(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record)).Timeout)
ctx, cancel := context.WithTimeout(c.Request.Context(), agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record)).Timeout)
defer cancel()
common.ApiSuccess(c, agnetRuntimeDiagnosticsForRecord(ctx, record))
common.ApiSuccess(c, agentRuntimeDiagnosticsForRecord(ctx, record))
}
func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploymentRecord, approvalID string) string {
func agentRuntimeApprovalDecisionPath(cfg agentRuntimeConfig, record agentDeploymentRecord, approvalID string) string {
path := strings.TrimSpace(cfg.ApprovalDecisionPath)
if path == "" {
path = "/api/swarms/{swarm_id}/approvals/{approval_id}"
@@ -858,8 +923,8 @@ func agnetRuntimeApprovalDecisionPath(cfg agnetRuntimeConfig, record agnetDeploy
return replacer.Replace(path)
}
func callAgnetRuntimeApprovalDecision(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, approval model.AgnetApprovalRequest, lease *model.AgnetCredentialLease, decision string) error {
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeApprovalDecisionPath(cfg, record, approval.ApprovalID))
func callAgentRuntimeApprovalDecision(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, approval model.AgentApprovalRequest, lease *model.AgentCredentialLease, decision string) error {
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeApprovalDecisionPath(cfg, record, approval.ApprovalID))
if err != nil {
return err
}
@@ -922,87 +987,87 @@ func callAgnetRuntimeApprovalDecision(ctx context.Context, cfg agnetRuntimeConfi
if err := common.Unmarshal(respBody, &envelope); err != nil {
return err
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return errors.New(message)
}
return nil
}
func syncAgnetRuntimeApprovalDecision(c *gin.Context, approval *model.AgnetApprovalRequest, lease *model.AgnetCredentialLease, decision string) {
func syncAgentRuntimeApprovalDecision(c *gin.Context, approval *model.AgentApprovalRequest, lease *model.AgentCredentialLease, decision string) {
if approval == nil || strings.TrimSpace(approval.DeploymentID) == "" {
return
}
if !agnetRuntimeClientConfigForMode(agnetRuntimeModeAgnet).Enabled && !agnetRuntimeClientConfigForMode(agnetRuntimeModeSwarm).Enabled {
if !agentRuntimeClientConfigForMode(agentRuntimeModeAgent).Enabled && !agentRuntimeClientConfigForMode(agentRuntimeModeSwarm).Enabled {
return
}
record, ok := findAgnetDeploymentRecord(approval.DeploymentID)
record, ok := findAgentDeploymentRecord(approval.DeploymentID)
if !ok {
recordAgnetApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "deployment not found")
recordAgentApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "deployment not found")
return
}
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if !cfg.Enabled {
return
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
recordAgnetApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "runtime identifiers missing")
recordAgentApprovalAudit("runtime.approval_decision.skipped", approval, lease, "skipped", "runtime identifiers missing")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
if err := callAgnetRuntimeApprovalDecision(ctx, cfg, record, *approval, lease, decision); err != nil {
recordAgnetApprovalAudit("runtime.approval_decision.failed", approval, lease, "failed", truncateAgnetFailureReason(err.Error()))
common.SysLog("Agnet runtime approval decision failed for " + approval.ApprovalID + ": " + err.Error())
if err := callAgentRuntimeApprovalDecision(ctx, cfg, record, *approval, lease, decision); err != nil {
recordAgentApprovalAudit("runtime.approval_decision.failed", approval, lease, "failed", truncateAgentFailureReason(err.Error()))
common.SysLog("Agent runtime approval decision failed for " + approval.ApprovalID + ": " + err.Error())
return
}
recordAgnetApprovalAudit("runtime.approval_decision.accepted", approval, lease, "ok", "")
recordAgentApprovalAudit("runtime.approval_decision.accepted", approval, lease, "ok", "")
}
func callAgnetRuntimeStop(ctx context.Context, cfg agnetRuntimeConfig, record agnetDeploymentRecord, reason string) (agnetRuntimeSyncResult, error) {
func callAgentRuntimeStop(ctx context.Context, cfg agentRuntimeConfig, record agentDeploymentRecord, reason string) (agentRuntimeSyncResult, error) {
runtimeDeploymentID := strings.TrimSpace(record.RuntimeDeploymentID)
if runtimeDeploymentID == "" {
return agnetRuntimeSyncResult{}, nil
return agentRuntimeSyncResult{}, nil
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, agnetRuntimeStopPathForRecord(cfg, record))
endpoint, err := agentRuntimeURL(cfg.BaseURL, agentRuntimeStopPathForRecord(cfg, record))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
payload, err := common.Marshal(gin.H{
"reason": firstNonEmpty(strings.TrimSpace(reason), "Heicode Manager requested stop"),
"manager_deployment_id": record.DeploymentID,
})
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
agnetRuntimeHeaders(req, cfg, record)
agentRuntimeHeaders(req, cfg, record)
client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req)
if err != nil {
return agnetRuntimeSyncResult{}, err
return agentRuntimeSyncResult{}, err
}
defer resp.Body.Close()
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if readErr != nil {
return agnetRuntimeSyncResult{}, readErr
return agentRuntimeSyncResult{}, readErr
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime stop returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, fmt.Errorf("runtime stop returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var envelope map[string]any
if len(body) > 0 {
if err := common.Unmarshal(body, &envelope); err != nil {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, err
}
}
if message := agnetRuntimeEnvelopeError(envelope); message != "" {
return agnetRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
if message := agentRuntimeEnvelopeError(envelope); message != "" {
return agentRuntimeSyncResult{RawStatusCode: resp.StatusCode}, errors.New(message)
}
data := extractAgnetRuntimeData(envelope)
return agnetRuntimeSyncResult{
data := extractAgentRuntimeData(envelope)
return agentRuntimeSyncResult{
RuntimeDeploymentID: firstNonEmpty(stringFromMap(data, "runtime_deployment_id", "deployment_id", "id"), runtimeDeploymentID),
RuntimeSwarmID: stringFromMap(data, "swarm_id", "runtime_swarm_id"),
RuntimeStatus: stringFromMap(data, "runtime_status", "status"),
@@ -1010,25 +1075,25 @@ func callAgnetRuntimeStop(ctx context.Context, cfg agnetRuntimeConfig, record ag
}, nil
}
func syncAgnetRuntimeStop(c *gin.Context, record agnetDeploymentRecord, reason string) (agnetDeploymentRecord, bool) {
cfg := agnetRuntimeClientConfigForMode(agnetRuntimeModeForRecord(record))
func syncAgentRuntimeStop(c *gin.Context, record agentDeploymentRecord, reason string) (agentDeploymentRecord, bool) {
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if !cfg.Enabled || strings.TrimSpace(record.RuntimeDeploymentID) == "" {
return record, true
}
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
result, err := callAgnetRuntimeStop(ctx, cfg, record, reason)
record.RuntimeLastSyncAt = agnetNow()
result, err := callAgentRuntimeStop(ctx, cfg, record, reason)
record.RuntimeLastSyncAt = agentNow()
if err != nil {
record.RuntimeState = agnetRuntimeStateFailed
record.FailureReason = truncateAgnetFailureReason(err.Error())
record.UpdatedAt = agnetNow()
_ = updateAgnetDeploymentRecord(record)
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
recordAgnetRuntimeSyncAudit(record, "runtime.stop.failed", "failed")
agnetError(c, "RUNTIME_STOP_FAILED", record.FailureReason)
record.RuntimeState = agentRuntimeStateFailed
record.FailureReason = truncateAgentFailureReason(err.Error())
record.UpdatedAt = agentNow()
_ = updateAgentDeploymentRecord(record)
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
recordAgentRuntimeSyncAudit(record, "runtime.stop.failed", "failed")
agentError(c, "RUNTIME_STOP_FAILED", record.FailureReason)
return record, false
}
record.RuntimeState = firstNonEmpty(result.RuntimeStatus, "stopped")
@@ -1039,33 +1104,33 @@ func syncAgnetRuntimeStop(c *gin.Context, record agnetDeploymentRecord, reason s
record.RuntimeSwarmID = result.RuntimeSwarmID
}
record.FailureReason = ""
recordAgnetRuntimeSyncAudit(record, "runtime.stop.accepted", "ok")
recordAgentRuntimeSyncAudit(record, "runtime.stop.accepted", "ok")
return record, true
}
func updateAgnetRuntimeSyncState(record agnetDeploymentRecord, result agnetRuntimeSyncResult, syncErr error) agnetDeploymentRecord {
record.RuntimeLastSyncAt = agnetNow()
func updateAgentRuntimeSyncState(record agentDeploymentRecord, result agentRuntimeSyncResult, syncErr error) agentDeploymentRecord {
record.RuntimeLastSyncAt = agentNow()
if syncErr != nil {
record.RuntimeState = agnetRuntimeStateFailed
record.FailureReason = truncateAgnetFailureReason(syncErr.Error())
record.RuntimeState = agentRuntimeStateFailed
record.FailureReason = truncateAgentFailureReason(syncErr.Error())
} else {
record.RuntimeState = firstNonEmpty(result.RuntimeStatus, agnetRuntimeStateSynced)
record.RuntimeState = firstNonEmpty(result.RuntimeStatus, agentRuntimeStateSynced)
record.RuntimeDeploymentID = result.RuntimeDeploymentID
record.RuntimeSwarmID = result.RuntimeSwarmID
record.FailureReason = ""
}
record.UpdatedAt = agnetNow()
if err := updateAgnetDeploymentRecord(record); err != nil {
common.SysLog("updateAgnetRuntimeSyncState: " + err.Error())
record.UpdatedAt = agentNow()
if err := updateAgentDeploymentRecord(record); err != nil {
common.SysLog("updateAgentRuntimeSyncState: " + err.Error())
}
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
return record
}
func recordAgnetRuntimeSyncAudit(record agnetDeploymentRecord, event string, result string) {
recordAgnetAuditEvent(agnetEvent{
func recordAgentRuntimeSyncAudit(record agentDeploymentRecord, event string, result string) {
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: event,
SchemaVersion: 1,
@@ -1074,79 +1139,79 @@ func recordAgnetRuntimeSyncAudit(record agnetDeploymentRecord, event string, res
BindingScope: firstPlanBindingScope(record.Plan),
DeploymentID: record.DeploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: agnetNow(),
}, "agnet_runtime_bridge", record.DeploymentID, "", result)
OccurredAt: agentNow(),
}, "agent_runtime_bridge", record.DeploymentID, "", result)
}
func dispatchAgnetRuntimeCreate(record agnetDeploymentRecord, source string, cfg agnetRuntimeConfig) agnetDeploymentRecord {
recordAgnetRuntimeSyncAudit(record, "runtime.sync.started", "started")
func dispatchAgentRuntimeCreate(record agentDeploymentRecord, source string, cfg agentRuntimeConfig) agentDeploymentRecord {
recordAgentRuntimeSyncAudit(record, "runtime.sync.started", "started")
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
result, err := callAgnetRuntimeCreate(ctx, cfg, record, source)
record = updateAgnetRuntimeSyncState(record, result, err)
result, err := callAgentRuntimeCreate(ctx, cfg, record, source)
record = updateAgentRuntimeSyncState(record, result, err)
if err != nil {
common.SysLog("Agnet runtime shadow create failed for " + record.DeploymentID + ": " + err.Error())
recordAgnetRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
common.SysLog("Agent runtime shadow create failed for " + record.DeploymentID + ": " + err.Error())
recordAgentRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
return record
}
recordAgnetRuntimeSyncAudit(record, "runtime.sync.accepted", "ok")
recordAgentRuntimeSyncAudit(record, "runtime.sync.accepted", "ok")
return record
}
func maybeDispatchAgnetRuntimeCreate(c *gin.Context, record agnetDeploymentRecord, source string) agnetDeploymentRecord {
mode := agnetRuntimeModeForSource(source)
func maybeDispatchAgentRuntimeCreate(c *gin.Context, record agentDeploymentRecord, source string) agentDeploymentRecord {
mode := agentRuntimeModeForSource(source)
if strings.TrimSpace(record.Plan.Metadata.RuntimeMode) == "" {
record.Plan.Metadata.RuntimeMode = mode
} else {
mode = agnetRuntimeModeForRecord(record)
mode = agentRuntimeModeForRecord(record)
}
cfg := agnetRuntimeClientConfigForMode(mode)
cfg := agentRuntimeClientConfigForMode(mode)
if !cfg.Enabled {
return record
}
if _, err := agnetRuntimeURL(cfg.BaseURL, cfg.CreatePath); err != nil {
record.RuntimeState = agnetRuntimeStateFailed
record.RuntimeLastSyncAt = agnetNow()
record.FailureReason = truncateAgnetFailureReason(err.Error())
record.UpdatedAt = agnetNow()
_ = updateAgnetDeploymentRecord(record)
recordAgnetRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
if _, err := agentRuntimeURL(cfg.BaseURL, cfg.CreatePath); err != nil {
record.RuntimeState = agentRuntimeStateFailed
record.RuntimeLastSyncAt = agentNow()
record.FailureReason = truncateAgentFailureReason(err.Error())
record.UpdatedAt = agentNow()
_ = updateAgentDeploymentRecord(record)
recordAgentRuntimeSyncAudit(record, "runtime.sync.failed", "failed")
return record
}
record.RuntimeState = agnetRuntimeStateSyncing
record.RuntimeLastSyncAt = agnetNow()
record.UpdatedAt = agnetNow()
if err := updateAgnetDeploymentRecord(record); err != nil {
common.SysLog("maybeDispatchAgnetRuntimeCreate: " + err.Error())
record.RuntimeState = agentRuntimeStateSyncing
record.RuntimeLastSyncAt = agentNow()
record.UpdatedAt = agentNow()
if err := updateAgentDeploymentRecord(record); err != nil {
common.SysLog("maybeDispatchAgentRuntimeCreate: " + err.Error())
}
agnetMu.Lock()
agnetDeployments[record.DeploymentID] = record
agnetMu.Unlock()
agentMu.Lock()
agentDeployments[record.DeploymentID] = record
agentMu.Unlock()
if cfg.Async {
sourceCopy := source
recordCopy := record
go dispatchAgnetRuntimeCreate(recordCopy, sourceCopy, cfg)
go dispatchAgentRuntimeCreate(recordCopy, sourceCopy, cfg)
return record
}
return dispatchAgnetRuntimeCreate(record, source, cfg)
return dispatchAgentRuntimeCreate(record, source, cfg)
}
func AgnetRuntimeHealth(c *gin.Context) {
cfg := agnetRuntimeClientConfigForMode(c.Query("mode"))
func AgentRuntimeHealth(c *gin.Context) {
cfg := agentRuntimeClientConfigForMode(c.Query("mode"))
data := gin.H{
"enabled": cfg.Enabled,
"configured": cfg.BaseURL != "",
"create_path": cfg.CreatePath,
"health_path": cfg.HealthPath,
"stop_path": cfg.StopPath,
"mode": normalizeAgnetRuntimeMode(c.Query("mode")),
"mode": normalizeAgentRuntimeMode(c.Query("mode")),
}
if cfg.BaseURL == "" {
data["status"] = "not_configured"
common.ApiSuccess(c, data)
return
}
endpoint, err := agnetRuntimeURL(cfg.BaseURL, cfg.HealthPath)
endpoint, err := agentRuntimeURL(cfg.BaseURL, cfg.HealthPath)
if err != nil {
data["status"] = "invalid_config"
data["message"] = err.Error()
+299
View File
@@ -0,0 +1,299 @@
package controller
import (
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
type agentTaskDeploymentDraftRequest struct {
Task agentTaskSnapshot `json:"task"`
SubMode string `json:"sub_mode"`
RiskLevel string `json:"risk_level"`
Budget agentBudget `json:"budget"`
BindingScope string `json:"binding_scope"`
RoleTemplates []string `json:"role_templates"`
DefaultModelID string `json:"default_model_id"`
RoleModels map[string]string `json:"role_models"`
ResourceGrants []agentResourceGrant `json:"resource_grants"`
}
type agentTaskSnapshot struct {
ID string `json:"id"`
Name string `json:"name"`
Intent string `json:"intent"`
Status string `json:"status"`
Card map[string]any `json:"card"`
}
func stringFromTaskCard(card map[string]any, key string) string {
if card == nil {
return ""
}
if value, ok := card[key].(string); ok {
return strings.TrimSpace(value)
}
return ""
}
func objectiveFromTaskSnapshot(task agentTaskSnapshot) string {
for _, value := range []string{
stringFromTaskCard(task.Card, "goal"),
task.Name,
task.Intent,
} {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func normalizeAgentDraftBudget(budget agentBudget) agentBudget {
if budget.MaxTokens <= 0 {
budget.MaxTokens = 120000
}
if budget.MaxCostUSD <= 0 {
budget.MaxCostUSD = 8
}
if budget.MaxDurationSec <= 0 {
budget.MaxDurationSec = 3600
}
return budget
}
func normalizeAgentDraftRoleTemplates(values []string) []string {
roles := make([]string, 0, len(values))
for _, value := range values {
role := strings.TrimSpace(value)
if role == "" {
continue
}
roles = append(roles, role)
}
if len(roles) == 0 {
return []string{"backend"}
}
return roles
}
func defaultAgentTaskBindingScope(taskID string) string {
bindingScope := "task-" + sanitizeAgentRef(taskID)
if bindingScope == "task-" {
return "task-local"
}
return bindingScope
}
func defaultTaskDraftResourceGrant(userID string, bindingScope string, role string, taskID string) agentResourceGrant {
return agentResourceGrant{
GrantID: "grant-" + sanitizeAgentRef(taskID) + "-" + sanitizeAgentRef(role),
ResourceID: "task-" + sanitizeAgentRef(taskID) + "-context",
ResourceType: agentResourceProjectDoc,
UserID: userID,
BindingScope: bindingScope,
TargetRole: role,
TargetAgentRef: "agent-" + sanitizeAgentRef(role) + "-1",
PermissionScope: []string{"doc:read"},
Constraints: map[string]string{"ref": "task-card"},
Metadata: map[string]string{"provider": "heicode-task", "resource_ref": taskID},
Status: agentGrantStatusActive,
Audit: map[string]string{"source": "heicode-task-draft"},
}
}
func sanitizeAgentRef(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
var b strings.Builder
for _, r := range value {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '-' || r == '_':
b.WriteRune(r)
default:
b.WriteRune('-')
}
}
return strings.Trim(b.String(), "-")
}
func buildAgentDraftAgentPlan(role string, defaultModelID string, grants []agentResourceGrant) agentAgentPlan {
if defaultModelID == "" {
defaultModelID = defaultAgentModelID()
}
return agentAgentPlan{
RoleTemplate: role,
Goal: fmt.Sprintf("Execute the Heicode task as %s within the approved resource scope.", role),
DefaultModelID: defaultModelID,
ResourceGrants: grants,
}
}
func normalizeTaskDraftResourceGrants(userID string, bindingScope string, role string, taskID string, grants []agentResourceGrant) []agentResourceGrant {
if len(grants) == 0 {
return []agentResourceGrant{defaultTaskDraftResourceGrant(userID, bindingScope, role, taskID)}
}
normalized := make([]agentResourceGrant, 0, len(grants))
for idx, grant := range grants {
grant.UserID = userID
if strings.TrimSpace(grant.GrantID) == "" {
grant.GrantID = fmt.Sprintf("grant-%s-%s-%d", sanitizeAgentRef(taskID), sanitizeAgentRef(role), idx+1)
}
if strings.TrimSpace(grant.BindingScope) == "" {
grant.BindingScope = bindingScope
}
if strings.TrimSpace(grant.TargetRole) == "" {
grant.TargetRole = role
}
if strings.TrimSpace(grant.TargetAgentRef) == "" {
grant.TargetAgentRef = "agent-" + sanitizeAgentRef(role) + "-1"
}
if strings.TrimSpace(grant.Status) == "" {
grant.Status = agentGrantStatusActive
}
grant = resolveResourceBindingIntoGrant(userID, grant)
normalized = append(normalized, grant)
}
return normalized
}
// resolveResourceBindingIntoGrant injects the stored ResourceBinding's real
// secret_ref and resource metadata when the client referenced a binding by id
// instead of inlining a secret_ref (unified spec §17.6). The binding must be
// owned by the requesting user; unknown/unowned ids are left untouched so the
// existing plan validation surfaces a clear error.
func resolveResourceBindingIntoGrant(userID string, grant agentResourceGrant) agentResourceGrant {
if grant.ResourceBindingID <= 0 || model.DB == nil {
return grant
}
uid, _ := strconv.Atoi(strings.TrimSpace(userID))
if uid <= 0 {
return grant
}
var binding model.ResourceBinding
if err := model.DB.Where("id = ? AND user_id = ?", grant.ResourceBindingID, uid).First(&binding).Error; err != nil {
return grant
}
if strings.TrimSpace(grant.SecretRef) == "" {
grant.SecretRef = strings.TrimSpace(binding.SecretRef)
}
if strings.TrimSpace(grant.ResourceID) == "" {
grant.ResourceID = fmt.Sprintf("rb_%d", binding.Id)
}
if strings.TrimSpace(grant.ResourceType) == "" {
grant.ResourceType = strings.TrimSpace(binding.ResourceType)
}
if strings.TrimSpace(grant.BindingScope) == "" {
grant.BindingScope = strings.TrimSpace(binding.BindingScope)
}
return grant
}
func AgentCreateTaskDeploymentDraft(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
agentError(c, "TASK_NOT_FOUND", "task_id is required")
return
}
var req agentTaskDeploymentDraftRequest
if err := c.ShouldBindJSON(&req); err != nil {
agentError(c, "POLICY_REJECTED", err.Error())
return
}
if strings.TrimSpace(req.Task.ID) == "" {
agentError(c, "TASK_NOT_FOUND", "task snapshot is required")
return
}
if strings.TrimSpace(req.Task.ID) != taskID {
agentError(c, "TASK_CONFLICT", "task snapshot id must match route task_id")
return
}
if !isValidAgentSubMode(req.SubMode) {
agentError(c, "POLICY_REJECTED", "sub_mode must be agile or waterfall")
return
}
userID := strconv.Itoa(c.GetInt("id"))
if userID == "0" {
agentError(c, "POLICY_REJECTED", "authenticated user is required")
return
}
objective := objectiveFromTaskSnapshot(req.Task)
if objective == "" {
agentError(c, "POLICY_REJECTED", "task objective is required")
return
}
bindingScope := strings.TrimSpace(req.BindingScope)
if bindingScope == "" {
bindingScope = defaultAgentTaskBindingScope(taskID)
}
roles := normalizeAgentDraftRoleTemplates(req.RoleTemplates)
riskLevel := strings.TrimSpace(req.RiskLevel)
if riskLevel == "" {
riskLevel = agentRiskLow
}
defaultModelID := strings.TrimSpace(req.DefaultModelID)
group := strings.TrimSpace(c.GetString("group"))
agents := make([]agentAgentPlan, 0, len(roles))
runtimeAgents := make([]agentRuntimeAgent, 0, len(roles))
// per_role model selection (unified spec §9): role_models[role] wins, then
// the request default, then the platform default. Every resolved model is
// collected into allowed_model_ids so create-time validation accepts them.
allowedSeen := map[string]bool{}
allowedModels := []string{}
addAllowedModel := func(m string) {
m = strings.TrimSpace(m)
if m == "" || allowedSeen[m] {
return
}
allowedSeen[m] = true
allowedModels = append(allowedModels, m)
}
for _, role := range roles {
grants := normalizeTaskDraftResourceGrants(userID, bindingScope, role, taskID, req.ResourceGrants)
modelRef := firstNonEmpty(req.RoleModels[role], defaultModelID, defaultAgentModelID())
agents = append(agents, buildAgentDraftAgentPlan(role, modelRef, grants))
runtimeAgents = append(runtimeAgents, agentRuntimeAgent{Role: role, ModelRef: modelRef, InstanceCount: 1})
addAllowedModel(modelRef)
}
plan := agentOrchestrationPlan{
IntentID: taskID,
TemplateHint: "heicode-task",
Objective: objective,
SubMode: normalizeAgentSubMode(req.SubMode),
RiskLevel: riskLevel,
Budget: normalizeAgentDraftBudget(req.Budget),
UserContext: agentUserContext{
UserID: userID,
Role: "user",
ChannelID: group,
},
AgentRuntime: agentAgentRuntime{Platform: "agent", Agents: runtimeAgents},
Agents: agents,
Constraints: agentConstraints{AllowedModelIDs: allowedModels},
Metadata: agentMetadata{
CorrelationID: "task-" + sanitizeAgentRef(taskID) + "-" + common.GetUUID()[:8],
},
}
if group != "" {
plan.BillingContext = agentBillingContext{Provider: "newapi", NewAPIGroup: group}
}
if !validateOrchestrationPlan(c, plan) {
return
}
common.ApiSuccess(c, gin.H{
"task_id": taskID,
"orchestration_plan": plan,
})
}
-253
View File
@@ -1,253 +0,0 @@
package controller
import (
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
)
type agnetTaskDeploymentDraftRequest struct {
Task agnetTaskSnapshot `json:"task"`
SubMode string `json:"sub_mode"`
RiskLevel string `json:"risk_level"`
Budget agnetBudget `json:"budget"`
BindingScope string `json:"binding_scope"`
RoleTemplates []string `json:"role_templates"`
DefaultModelID string `json:"default_model_id"`
ResourceGrants []agnetResourceGrant `json:"resource_grants"`
}
type agnetTaskSnapshot struct {
ID string `json:"id"`
Name string `json:"name"`
Intent string `json:"intent"`
Status string `json:"status"`
Card map[string]any `json:"card"`
}
func stringFromTaskCard(card map[string]any, key string) string {
if card == nil {
return ""
}
if value, ok := card[key].(string); ok {
return strings.TrimSpace(value)
}
return ""
}
func objectiveFromTaskSnapshot(task agnetTaskSnapshot) string {
for _, value := range []string{
stringFromTaskCard(task.Card, "goal"),
task.Name,
task.Intent,
} {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func normalizeAgnetDraftBudget(budget agnetBudget) agnetBudget {
if budget.MaxTokens <= 0 {
budget.MaxTokens = 120000
}
if budget.MaxCostUSD <= 0 {
budget.MaxCostUSD = 8
}
if budget.MaxDurationSec <= 0 {
budget.MaxDurationSec = 3600
}
return budget
}
func normalizeAgnetDraftRoleTemplates(values []string) []string {
roles := make([]string, 0, len(values))
for _, value := range values {
role := strings.TrimSpace(value)
if role == "" {
continue
}
roles = append(roles, role)
}
if len(roles) == 0 {
return []string{"backend"}
}
return roles
}
func defaultAgnetTaskBindingScope(taskID string) string {
bindingScope := "task-" + sanitizeAgnetRef(taskID)
if bindingScope == "task-" {
return "task-local"
}
return bindingScope
}
func defaultTaskDraftResourceGrant(userID string, bindingScope string, role string, taskID string) agnetResourceGrant {
return agnetResourceGrant{
GrantID: "grant-" + sanitizeAgnetRef(taskID) + "-" + sanitizeAgnetRef(role),
ResourceID: "task-" + sanitizeAgnetRef(taskID) + "-context",
ResourceType: agnetResourceProjectDoc,
UserID: userID,
BindingScope: bindingScope,
TargetRole: role,
TargetAgentRef: "agent-" + sanitizeAgnetRef(role) + "-1",
PermissionScope: []string{"doc:read"},
Constraints: map[string]string{"ref": "task-card"},
Metadata: map[string]string{"provider": "heicode-task", "resource_ref": taskID},
Status: agnetGrantStatusActive,
Audit: map[string]string{"source": "heicode-task-draft"},
}
}
func sanitizeAgnetRef(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
var b strings.Builder
for _, r := range value {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '-' || r == '_':
b.WriteRune(r)
default:
b.WriteRune('-')
}
}
return strings.Trim(b.String(), "-")
}
func buildAgnetDraftAgentPlan(role string, defaultModelID string, grants []agnetResourceGrant) agnetAgentPlan {
if defaultModelID == "" {
defaultModelID = defaultAgnetModelID()
}
return agnetAgentPlan{
RoleTemplate: role,
Goal: fmt.Sprintf("Execute the Heicode task as %s within the approved resource scope.", role),
DefaultModelID: defaultModelID,
ResourceGrants: grants,
}
}
func normalizeTaskDraftResourceGrants(userID string, bindingScope string, role string, taskID string, grants []agnetResourceGrant) []agnetResourceGrant {
if len(grants) == 0 {
return []agnetResourceGrant{defaultTaskDraftResourceGrant(userID, bindingScope, role, taskID)}
}
normalized := make([]agnetResourceGrant, 0, len(grants))
for idx, grant := range grants {
grant.UserID = userID
if strings.TrimSpace(grant.GrantID) == "" {
grant.GrantID = fmt.Sprintf("grant-%s-%s-%d", sanitizeAgnetRef(taskID), sanitizeAgnetRef(role), idx+1)
}
if strings.TrimSpace(grant.BindingScope) == "" {
grant.BindingScope = bindingScope
}
if strings.TrimSpace(grant.TargetRole) == "" {
grant.TargetRole = role
}
if strings.TrimSpace(grant.TargetAgentRef) == "" {
grant.TargetAgentRef = "agent-" + sanitizeAgnetRef(role) + "-1"
}
if strings.TrimSpace(grant.Status) == "" {
grant.Status = agnetGrantStatusActive
}
normalized = append(normalized, grant)
}
return normalized
}
func AgnetCreateTaskDeploymentDraft(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
agnetError(c, "TASK_NOT_FOUND", "task_id is required")
return
}
var req agnetTaskDeploymentDraftRequest
if err := c.ShouldBindJSON(&req); err != nil {
agnetError(c, "POLICY_REJECTED", err.Error())
return
}
if strings.TrimSpace(req.Task.ID) == "" {
agnetError(c, "TASK_NOT_FOUND", "task snapshot is required")
return
}
if strings.TrimSpace(req.Task.ID) != taskID {
agnetError(c, "TASK_CONFLICT", "task snapshot id must match route task_id")
return
}
if !isValidAgnetSubMode(req.SubMode) {
agnetError(c, "POLICY_REJECTED", "sub_mode must be agile or waterfall")
return
}
userID := strconv.Itoa(c.GetInt("id"))
if userID == "0" {
agnetError(c, "POLICY_REJECTED", "authenticated user is required")
return
}
objective := objectiveFromTaskSnapshot(req.Task)
if objective == "" {
agnetError(c, "POLICY_REJECTED", "task objective is required")
return
}
bindingScope := strings.TrimSpace(req.BindingScope)
if bindingScope == "" {
bindingScope = defaultAgnetTaskBindingScope(taskID)
}
roles := normalizeAgnetDraftRoleTemplates(req.RoleTemplates)
riskLevel := strings.TrimSpace(req.RiskLevel)
if riskLevel == "" {
riskLevel = agnetRiskLow
}
defaultModelID := strings.TrimSpace(req.DefaultModelID)
group := strings.TrimSpace(c.GetString("group"))
agents := make([]agnetAgentPlan, 0, len(roles))
runtimeAgents := make([]agnetRuntimeAgent, 0, len(roles))
for _, role := range roles {
grants := normalizeTaskDraftResourceGrants(userID, bindingScope, role, taskID, req.ResourceGrants)
agents = append(agents, buildAgnetDraftAgentPlan(role, defaultModelID, grants))
modelRef := defaultModelID
if modelRef == "" {
modelRef = defaultAgnetModelID()
}
runtimeAgents = append(runtimeAgents, agnetRuntimeAgent{Role: role, ModelRef: modelRef, InstanceCount: 1})
}
plan := agnetOrchestrationPlan{
IntentID: taskID,
TemplateHint: "heicode-task",
Objective: objective,
SubMode: normalizeAgnetSubMode(req.SubMode),
RiskLevel: riskLevel,
Budget: normalizeAgnetDraftBudget(req.Budget),
UserContext: agnetUserContext{
UserID: userID,
Role: "user",
ChannelID: group,
},
AgentRuntime: agnetAgentRuntime{Platform: "agnet", Agents: runtimeAgents},
Agents: agents,
Constraints: agnetConstraints{AllowedModelIDs: []string{}},
Metadata: agnetMetadata{
CorrelationID: "task-" + sanitizeAgnetRef(taskID) + "-" + common.GetUUID()[:8],
},
}
if group != "" {
plan.BillingContext = agnetBillingContext{Provider: "newapi", NewAPIGroup: group}
}
if !validateOrchestrationPlan(c, plan) {
return
}
common.ApiSuccess(c, gin.H{
"task_id": taskID,
"orchestration_plan": plan,
})
}
@@ -20,14 +20,14 @@ import (
"gorm.io/gorm"
)
// HeicodeAgnetSessionRequest accepts tokens obtained only from Agnet identity platform.
// HeicodeAgentSessionRequest accepts tokens obtained only from Agent identity platform.
// Manager verifies them server-side and issues the browser session cookie (same as password login).
type HeicodeAgnetSessionRequest struct {
type HeicodeAgentSessionRequest struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type agnetMeEnvelope struct {
type agentMeEnvelope struct {
Success bool `json:"success"`
Message string `json:"message"`
Detail string `json:"detail"`
@@ -41,7 +41,7 @@ type agnetMeEnvelope struct {
} `json:"data"`
}
type agnetRefreshEnvelope struct {
type agentRefreshEnvelope struct {
Success bool `json:"success"`
Detail string `json:"detail"`
Message string `json:"message"`
@@ -80,10 +80,10 @@ func parseEmailList(raw string) map[string]struct{} {
return out
}
// roleFromAgnetWithEmail decides the local role for a JIT-synced Agnet user.
// roleFromAgentWithEmail decides the local role for a JIT-synced Agent user.
//
// 安全策略:管理员权限只能通过本地配置(环境变量白名单)显式授予,
// **不信任** Agnet 平台返回的 role 字段。这样防止外部身份平台
// **不信任** Agent 平台返回的 role 字段。这样防止外部身份平台
// 的角色被直接映射到 Manager 的高权限角色。
//
// - 邮箱命中 HEICODE_ROOT_EMAILS -> RoleRootUser
@@ -92,7 +92,7 @@ func parseEmailList(raw string) map[string]struct{} {
//
// 第二参数 `role` 当前未使用,保留是为了未来扩展(例如在策略中允许
// 信任部分上游 role),不破坏调用点签名。
func roleFromAgnetWithEmail(_ string, email string) int {
func roleFromAgentWithEmail(_ string, email string) int {
emailKey := strings.ToLower(strings.TrimSpace(email))
rootEmails := parseEmailList(os.Getenv("HEICODE_ROOT_EMAILS"))
if _, ok := rootEmails[emailKey]; ok {
@@ -105,26 +105,26 @@ func roleFromAgnetWithEmail(_ string, email string) int {
return common.RoleCommonUser
}
func statusFromAgnet(status string) int {
func statusFromAgent(status string) int {
if strings.EqualFold(strings.TrimSpace(status), "active") {
return common.UserStatusEnabled
}
return common.UserStatusDisabled
}
func agnetHTTPClient() *http.Client {
func agentHTTPClient() *http.Client {
return &http.Client{Timeout: 15 * time.Second}
}
func fetchAgnetMe(baseURL, accessToken string) (agnetMeEnvelope, int, error) {
var out agnetMeEnvelope
func fetchAgentMe(baseURL, accessToken string) (agentMeEnvelope, int, error) {
var out agentMeEnvelope
req, err := http.NewRequest(http.MethodGet, baseURL+"/api/auth/me", nil)
if err != nil {
return out, 0, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-Request-Id", common.GetUUID())
res, err := agnetHTTPClient().Do(req)
res, err := agentHTTPClient().Do(req)
if err != nil {
return out, 0, err
}
@@ -134,12 +134,12 @@ func fetchAgnetMe(baseURL, accessToken string) (agnetMeEnvelope, int, error) {
return out, res.StatusCode, err
}
if err := common.Unmarshal(body, &out); err != nil {
return out, res.StatusCode, fmt.Errorf("invalid response from Agnet /me: %w", err)
return out, res.StatusCode, fmt.Errorf("invalid response from Agent /me: %w", err)
}
return out, res.StatusCode, nil
}
func fetchAgnetRefresh(baseURL, refreshToken string) (access string, refresh string, err error) {
func fetchAgentRefresh(baseURL, refreshToken string) (access string, refresh string, err error) {
req, err := http.NewRequest(http.MethodPost, baseURL+"/api/auth/refresh", nil)
if err != nil {
return "", "", err
@@ -147,7 +147,7 @@ func fetchAgnetRefresh(baseURL, refreshToken string) (access string, refresh str
req.Header.Set("Authorization", "Bearer "+refreshToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-Id", common.GetUUID())
res, err := agnetHTTPClient().Do(req)
res, err := agentHTTPClient().Do(req)
if err != nil {
return "", "", err
}
@@ -156,9 +156,9 @@ func fetchAgnetRefresh(baseURL, refreshToken string) (access string, refresh str
if err != nil {
return "", "", err
}
var env agnetRefreshEnvelope
var env agentRefreshEnvelope
if err := common.Unmarshal(body, &env); err != nil {
return "", "", fmt.Errorf("invalid response from Agnet /refresh: %w", err)
return "", "", fmt.Errorf("invalid response from Agent /refresh: %w", err)
}
if !env.Success || env.Data.Token == "" {
msg := env.Message
@@ -212,7 +212,7 @@ func markBillingProviderNewapi(email string) {
req.Header.Set("Authorization", "Bearer "+tok)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-Id", common.GetUUID())
res, err := agnetHTTPClient().Do(req)
res, err := agentHTTPClient().Do(req)
if err != nil {
common.SysLog("markBillingProviderNewapi: send failed: " + err.Error())
return
@@ -228,10 +228,10 @@ func markBillingProviderNewapi(email string) {
}(email, tok)
}
func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
func syncLocalUserFromAgent(me agentMeEnvelope) (*model.User, error) {
email := strings.TrimSpace(me.Data.Email)
if email == "" {
return nil, errors.New("Agnet account has no email")
return nil, errors.New("Agent account has no email")
}
var user model.User
@@ -241,7 +241,7 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
if display == "" {
display = strings.Split(email, "@")[0]
}
// Local model-access bucket — never seed with the Agnet channelId
// Local model-access bucket — never seed with the Agent channelId
// (no abilities row matches a random UUID, so the new user would land
// with zero models on first /v1/models call). Admins control group
// from the NewAPI dashboard after JIT-create. Fix companion to
@@ -253,12 +253,12 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
Password: common.GetRandomString(32),
DisplayName: display,
Email: email,
Role: roleFromAgnetWithEmail(me.Data.Role, email),
Status: statusFromAgnet(me.Data.Status),
Role: roleFromAgentWithEmail(me.Data.Role, email),
Status: statusFromAgent(me.Data.Status),
Group: group,
}
if nu.Status != common.UserStatusEnabled {
return nil, errors.New("Agnet account is not active")
return nil, errors.New("Agent account is not active")
}
if err := nu.Insert(0); err != nil {
// Possible race: duplicate email/username — reload.
@@ -283,10 +283,10 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
user.DisplayName = name
changed = true
}
// Don't overwrite the existing user's group with the Agnet channelId on
// Don't overwrite the existing user's group with the Agent channelId on
// every login: NewAPI's `users.group` is the **local model-access bucket**
// (must match a row in the `abilities` / `channels` group column to expose
// any models). The Agnet channelId is a cross-platform identity that
// any models). The Agent channelId is a cross-platform identity that
// rarely matches a NewAPI-side group, so overwriting strands the user
// with zero models. mcp-server side already tracks channelId separately
// (see markBillingProviderNewapi), so we don't need it duplicated here.
@@ -295,8 +295,8 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
// is new and has no admin-set group yet). After that, NewAPI admins own
// the group via the dashboard.
_ = me.Data.ChannelID
// Promote role from Agnet / email whitelist on every login (never demote).
desiredRole := roleFromAgnetWithEmail(me.Data.Role, email)
// Promote role from Agent / email whitelist on every login (never demote).
desiredRole := roleFromAgentWithEmail(me.Data.Role, email)
if desiredRole > user.Role {
user.Role = desiredRole
changed = true
@@ -313,9 +313,9 @@ func syncLocalUserFromAgnet(me agnetMeEnvelope) (*model.User, error) {
return &user, nil
}
// HeicodeAgnetSessionLogin establishes Manager session after Agnet identity verified via token(s).
func HeicodeAgnetSessionLogin(c *gin.Context) {
var req HeicodeAgnetSessionRequest
// HeicodeAgentSessionLogin establishes Manager session after Agent identity verified via token(s).
func HeicodeAgentSessionLogin(c *gin.Context) {
var req HeicodeAgentSessionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid params"})
return
@@ -328,19 +328,19 @@ func HeicodeAgnetSessionLogin(c *gin.Context) {
}
baseURL := defaultHeicodeAuthBaseURL()
me, status, err := fetchAgnetMe(baseURL, access)
me, status, err := fetchAgentMe(baseURL, access)
newAccess := ""
newRefresh := ""
if (err != nil || status == http.StatusUnauthorized || !me.Success) && refresh != "" {
na, nr, refErr := fetchAgnetRefresh(baseURL, refresh)
na, nr, refErr := fetchAgentRefresh(baseURL, refresh)
if refErr != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": refErr.Error()})
return
}
newAccess = na
newRefresh = nr
me, _, err = fetchAgnetMe(baseURL, newAccess)
me, _, err = fetchAgentMe(baseURL, newAccess)
}
if err != nil {
@@ -353,13 +353,13 @@ func HeicodeAgnetSessionLogin(c *gin.Context) {
msg = me.Detail
}
if msg == "" {
msg = "unable to verify identity with Agnet"
msg = "unable to verify identity with Agent"
}
c.JSON(http.StatusOK, gin.H{"success": false, "message": msg})
return
}
user, err := syncLocalUserFromAgnet(me)
user, err := syncLocalUserFromAgent(me)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
return
@@ -0,0 +1,50 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
)
// HeicodeCapabilities exposes the client-facing mode + model catalog so the
// desktop client can discover which modes (Sub Agile / Swarm) are available and
// how each selects models (unified spec §6: GET /api/heicode/capabilities).
//
// It is intentionally unauthenticated catalog data: it returns no user-specific
// information, only the static mode contract plus whether each runtime is
// currently wired, and the production-aligned default model.
func HeicodeCapabilities(c *gin.Context) {
subCfg := agentRuntimeClientConfigForMode(agentRuntimeModeAgent)
swarmCfg := agentRuntimeClientConfigForMode(agentRuntimeModeSwarm)
defaultModel := defaultAgentModelID()
common.ApiSuccess(c, gin.H{
"modes": []gin.H{
{
"id": "sub_agile",
"name": "Sub Agile",
"runtime_kind": "agent_management",
"model_selection": "per_role",
"supports_roles": true,
"supports_task_graph": false,
"supports_artifacts": true,
"supports_continue_chat": true,
"enabled": subCfg.Enabled,
},
{
"id": "swarm",
"name": "Swarm",
"runtime_kind": "heicode_swarm",
"model_selection": "primary",
"supports_roles": false,
"supports_task_graph": true,
"supports_artifacts": true,
"supports_continue_chat": true,
"enabled": swarmCfg.Enabled,
},
},
"models": []gin.H{
{"id": defaultModel, "name": defaultModel, "available": true},
},
})
}
@@ -0,0 +1,59 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// HeicodeTaskWorkflow projects a deployment into the client-facing "workflow"
// shape consumed by the desktop right-hand task panel (unified spec §7.9 /
// §10.2). task_id is the deployment_id. Status is the Manager-judged
// display_status, so the client never has to interpret raw runtime state.
func HeicodeTaskWorkflow(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
record = withDisplayStatus(record)
artifacts, _ := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
artItems := make([]gin.H, 0, len(artifacts))
for _, a := range artifacts {
artItems = append(artItems, gin.H{
"artifact_id": a.ArtifactID,
"title": a.Title,
"artifact_type": a.ArtifactType,
"summary": a.Summary,
})
}
agents := make([]gin.H, 0, len(record.AgentInstances))
for _, inst := range record.AgentInstances {
agents = append(agents, gin.H{
"agent_id": inst.InstanceID,
"name": inst.Role,
"role": inst.Role,
"status": firstNonEmpty(inst.RuntimeState, inst.Phase),
})
}
common.ApiSuccess(c, gin.H{
"workflow_id": record.DeploymentID,
"task_id": record.DeploymentID,
"deployment_id": record.DeploymentID,
"mode": firstNonEmpty(record.SubMode, "agile"),
"title": firstNonEmpty(record.Plan.Objective, record.DeploymentID),
"summary": record.Plan.Objective,
"status": record.DisplayStatus,
"display_status": record.DisplayStatus,
"phase": record.Phase,
"agent_count": len(record.AgentInstances),
"agents": agents,
"artifacts": artItems,
})
}
+12 -12
View File
@@ -94,7 +94,7 @@ type resourceGrantPayload struct {
BindingScope string `json:"binding_scope"`
ResourceId int `json:"resource_id"`
Role string `json:"role"`
AgnetId string `json:"agnet_id"`
AgentId string `json:"agent_id"`
PermissionScope map[string]any `json:"permission_scope"`
Constraints map[string]any `json:"constraints"`
Status string `json:"status"`
@@ -108,7 +108,7 @@ type resourceGrantResponse struct {
BindingScope string `json:"binding_scope"`
ResourceId int `json:"resource_id"`
Role string `json:"role"`
AgnetId string `json:"agnet_id"`
AgentId string `json:"agent_id"`
PermissionScope map[string]any `json:"permission_scope"`
Constraints map[string]any `json:"constraints"`
Status string `json:"status"`
@@ -190,7 +190,7 @@ func normalizeResourceGrantPayload(p resourceGrantPayload) (resourceGrantPayload
p.ProjectId = strings.TrimSpace(p.ProjectId)
p.BindingScope = strings.TrimSpace(p.BindingScope)
p.Role = strings.TrimSpace(p.Role)
p.AgnetId = strings.TrimSpace(p.AgnetId)
p.AgentId = strings.TrimSpace(p.AgentId)
p.Status = strings.ToLower(strings.TrimSpace(p.Status))
if p.ResourceId <= 0 {
@@ -199,8 +199,8 @@ func normalizeResourceGrantPayload(p resourceGrantPayload) (resourceGrantPayload
if p.Role == "" {
return p, errors.New("role required")
}
if p.AgnetId == "" {
return p, errors.New("agnet_id required")
if p.AgentId == "" {
return p, errors.New("agent_id required")
}
if p.Status == "" {
p.Status = "active"
@@ -375,7 +375,7 @@ func resourceGrantToResponse(grant model.ResourceGrant, resource *model.Resource
BindingScope: grant.BindingScope,
ResourceId: grant.ResourceId,
Role: grant.Role,
AgnetId: grant.AgnetId,
AgentId: grant.AgentId,
PermissionScope: unmarshalResourceJSON(grant.PermissionScope),
Constraints: unmarshalResourceJSON(grant.Constraints),
Status: grant.Status,
@@ -719,8 +719,8 @@ func ListResourceGrants(c *gin.Context) {
if bindingScope := strings.TrimSpace(c.Query("binding_scope")); bindingScope != "" {
query = query.Where("binding_scope = ?", bindingScope)
}
if agnetId := strings.TrimSpace(c.Query("agnet_id")); agnetId != "" {
query = query.Where("agnet_id = ?", agnetId)
if agentId := strings.TrimSpace(c.Query("agent_id")); agentId != "" {
query = query.Where("agent_id = ?", agentId)
}
if resourceId := strings.TrimSpace(c.Query("resource_id")); resourceId != "" {
query = query.Where("resource_id = ?", resourceId)
@@ -743,7 +743,7 @@ func ListResourceGrants(c *gin.Context) {
func GenerateResourceGrantManifest(c *gin.Context) {
userId := c.GetInt("id")
bindingScope := strings.TrimSpace(c.Query("binding_scope"))
agentId := strings.TrimSpace(c.Query("agnet_id"))
agentId := strings.TrimSpace(c.Query("agent_id"))
role := strings.TrimSpace(c.Query("role"))
query := model.DB.Where("user_id = ? AND status = ?", userId, "active")
@@ -751,7 +751,7 @@ func GenerateResourceGrantManifest(c *gin.Context) {
query = query.Where("binding_scope = ?", bindingScope)
}
if agentId != "" {
query = query.Where("agnet_id = ?", agentId)
query = query.Where("agent_id = ?", agentId)
}
if role != "" {
query = query.Where("role = ?", role)
@@ -815,7 +815,7 @@ func CreateResourceGrant(c *gin.Context) {
BindingScope: payload.BindingScope,
ResourceId: payload.ResourceId,
Role: payload.Role,
AgnetId: payload.AgnetId,
AgentId: payload.AgentId,
PermissionScope: permissionScope,
Constraints: constraints,
Status: payload.Status,
@@ -864,7 +864,7 @@ func UpdateResourceGrant(c *gin.Context) {
grant.BindingScope = payload.BindingScope
grant.ResourceId = payload.ResourceId
grant.Role = payload.Role
grant.AgnetId = payload.AgnetId
grant.AgentId = payload.AgentId
grant.PermissionScope = permissionScope
grant.Constraints = constraints
grant.Status = payload.Status
+11 -11
View File
@@ -120,7 +120,7 @@ func TestDeleteResourceRevokesBindingAndActiveGrants(t *testing.T) {
BindingScope: "project-alpha",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agent-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["repo:read"]}`,
Status: "active",
}
@@ -159,7 +159,7 @@ func TestDeleteResourceRevokesBindingAndActiveGrants(t *testing.T) {
require.Contains(t, listActive.Body.String(), `"items":[]`)
}
func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
func TestCreateResourceGrantAssignsBoundResourceToRoleAgent(t *testing.T) {
db := setupResourceControllerTestDB(t)
resource := model.ResourceBinding{
UserId: 7,
@@ -177,7 +177,7 @@ func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
"binding_scope":"https://example.com/sk.git#main",
"resource_id":%d,
"role":"developer",
"agnet_id":"agnet-dev-1",
"agent_id":"agent-dev-1",
"permission_scope":{"actions":["read"]},
"constraints":{"paths":["skills/**"]}
}`, resource.Id)
@@ -187,14 +187,14 @@ func TestCreateResourceGrantAssignsBoundResourceToRoleAgnet(t *testing.T) {
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"https://example.com/sk.git#main"`)
require.Contains(t, w.Body.String(), `"role":"developer"`)
require.Contains(t, w.Body.String(), `"agnet_id":"agnet-dev-1"`)
require.Contains(t, w.Body.String(), `"agent_id":"agent-dev-1"`)
var grant model.ResourceGrant
require.NoError(t, db.First(&grant).Error)
require.Equal(t, resource.Id, grant.ResourceId)
require.Equal(t, "https://example.com/sk.git#main", grant.BindingScope)
require.Equal(t, "developer", grant.Role)
require.Equal(t, "agnet-dev-1", grant.AgnetId)
require.Equal(t, "agent-dev-1", grant.AgentId)
}
func TestCreateResourceGrantRejectsMismatchedBindingScope(t *testing.T) {
@@ -212,7 +212,7 @@ func TestCreateResourceGrantRejectsMismatchedBindingScope(t *testing.T) {
"binding_scope":"azure-vm-dev",
"resource_id":%d,
"role":"operator",
"agnet_id":"agnet-ops-1"
"agent_id":"agent-ops-1"
}`, resource.Id)
w := performResourceRequest(CreateResourceGrant, 7, http.MethodPost, "/", body)
@@ -241,7 +241,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
BindingScope: "repo-main",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agnet-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["read"]}`,
Constraints: `{"paths":["heicode/controller/**"]}`,
Status: "active",
@@ -251,7 +251,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
BindingScope: "repo-main",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agnet-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["write"]}`,
Status: "revoked",
}).Error)
@@ -261,7 +261,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
7,
http.MethodGet,
"/manifest",
"/manifest?binding_scope=repo-main&role=backend&agnet_id=agnet-backend-1",
"/manifest?binding_scope=repo-main&role=backend&agent_id=agent-backend-1",
"",
)
@@ -269,7 +269,7 @@ func TestGenerateResourceGrantManifestIncludesActiveGrantsOnly(t *testing.T) {
require.Contains(t, w.Body.String(), `"success":true`)
require.Contains(t, w.Body.String(), `"binding_scope":"repo-main"`)
require.Contains(t, w.Body.String(), `"agent_role":"backend"`)
require.Contains(t, w.Body.String(), `"target_agent_ref":"agnet-backend-1"`)
require.Contains(t, w.Body.String(), `"target_agent_ref":"agent-backend-1"`)
require.Contains(t, w.Body.String(), `"resource_type":"git"`)
require.Contains(t, w.Body.String(), `"allowed_actions":["read"]`)
require.Contains(t, w.Body.String(), `"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/repo-main"`)
@@ -293,7 +293,7 @@ func TestDeleteResourceGrantRevokesInsteadOfDeleting(t *testing.T) {
BindingScope: "project-alpha",
ResourceId: resource.Id,
Role: "backend",
AgnetId: "agent-backend-1",
AgentId: "agent-backend-1",
PermissionScope: `{"actions":["repo:read","repo:write"]}`,
Constraints: `{"paths":["src/**"]}`,
Status: "active",
+10 -10
View File
@@ -45,7 +45,7 @@ services:
- NODE_NAME=heicode-node-1
# 默认与 docs/integration/Heicode-登录接口对接文档.md §2.1 一致;覆盖仅用于非标准网关。
- HEICODE_AUTH_BASE_URL=${HEICODE_AUTH_BASE_URL:-https://apimtaiji.azure-api.net/api/mcp}
# Agnet 登录后 JIT 同步:邮箱命中以下白名单则自动提权
# Agent 登录后 JIT 同步:邮箱命中以下白名单则自动提权
- HEICODE_ROOT_EMAILS=${HEICODE_ROOT_EMAILS:-}
- HEICODE_ADMIN_EMAILS=${HEICODE_ADMIN_EMAILS:-}
# Long-lived resource credentials are written to Azure Key Vault via
@@ -54,18 +54,18 @@ services:
- AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}
# Agent Manager Runtime: use the current production IP directly.
# The public domain is not used until DNS/HTTPS is fixed.
- AGNET_RUNTIME_ENABLED=${AGNET_RUNTIME_ENABLED:-false}
- AGNET_RUNTIME_BASE_URL=${AGNET_RUNTIME_BASE_URL:-http://20.212.121.126}
- AGNET_RUNTIME_CREATE_PATH=${AGNET_RUNTIME_CREATE_PATH:-/api/agnet/deployments}
- AGNET_RUNTIME_HEALTH_PATH=${AGNET_RUNTIME_HEALTH_PATH:-/api/agnet/health}
- AGNET_RUNTIME_STOP_PATH=${AGNET_RUNTIME_STOP_PATH:-/api/agnet/deployments/{deployment_id}/stop}
- AGNET_RUNTIME_SERVICE_TOKEN=${AGNET_RUNTIME_SERVICE_TOKEN:-}
- AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF=${AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF:-}
# HeiCode-Swarm Runtime is separate from ordinary sub Agnet Runtime.
- AGENT_RUNTIME_ENABLED=${AGENT_RUNTIME_ENABLED:-false}
- AGENT_RUNTIME_BASE_URL=${AGENT_RUNTIME_BASE_URL:-http://20.212.121.126}
- AGENT_RUNTIME_CREATE_PATH=${AGENT_RUNTIME_CREATE_PATH:-/api/agent/deployments}
- AGENT_RUNTIME_HEALTH_PATH=${AGENT_RUNTIME_HEALTH_PATH:-/api/agent/health}
- AGENT_RUNTIME_STOP_PATH=${AGENT_RUNTIME_STOP_PATH:-/api/agent/deployments/{deployment_id}/stop}
- AGENT_RUNTIME_SERVICE_TOKEN=${AGENT_RUNTIME_SERVICE_TOKEN:-}
- AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF=${AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF:-}
# HeiCode-Swarm Runtime is separate from ordinary sub Agent Runtime.
- SWARM_RUNTIME_ENABLED=${SWARM_RUNTIME_ENABLED:-false}
- SWARM_RUNTIME_BASE_URL=${SWARM_RUNTIME_BASE_URL:-}
- SWARM_RUNTIME_CREATE_PATH=${SWARM_RUNTIME_CREATE_PATH:-/api/swarms}
- SWARM_RUNTIME_HEALTH_PATH=${SWARM_RUNTIME_HEALTH_PATH:-/api/agnet/health}
- SWARM_RUNTIME_HEALTH_PATH=${SWARM_RUNTIME_HEALTH_PATH:-/api/agent/health}
- SWARM_RUNTIME_STOP_PATH=${SWARM_RUNTIME_STOP_PATH:-/api/swarms/{swarm_id}/stop}
- SWARM_RUNTIME_APPROVAL_DECISION_PATH=${SWARM_RUNTIME_APPROVAL_DECISION_PATH:-/api/swarms/{swarm_id}/approvals/{approval_id}}
- SWARM_RUNTIME_SERVICE_TOKEN=${SWARM_RUNTIME_SERVICE_TOKEN:-}
@@ -12,7 +12,7 @@ Security rule: never put real passwords, tokens, SSH keys, Redis keys, PostgreSQ
| `heicode/docker-compose.azure-vm.yml` | Azure VM Manager service | Runs only `heicode`; PostgreSQL and Redis are expected to be managed Azure services. |
| `heicode/docker-compose.override.yml` | Local-source image override | Builds `heicode-manager:local` from the checked-out repo. Keep it in the compose file list when deploying this repo state. |
| `heicode/bin/azure_vm_deploy.sh` | SSH deployment helper | Uses env vars only; can fast-forward a remote branch, performs remote compose up, health gate, and rollback pointer capture. |
| `heicode/bin/acceptance_agnet_local.sh` | Local Agnet control-plane smoke/acceptance probe | Requires an admin session cookie supplied via env; does not store credentials. |
| `heicode/bin/acceptance_agent_local.sh` | Local Agent control-plane smoke/acceptance probe | Requires an admin session cookie supplied via env; does not store credentials. |
| `heicode/.env.example` | Env-var reference | Placeholder-only reference; production `.env` must stay on the VM and out of Git. |
## 2. Required VM inputs
@@ -114,9 +114,9 @@ The script will:
6. Poll `/api/status` through the VM-local health URL.
7. Update `.last_success_image` only after the health gate passes.
## 5.1 Agnet operator handoff
## 5.1 Agent operator handoff
When Agnet is the executor, Manager should create a high-risk `newapi-rebuild-deploy` deployment using `docs/integration/agnet-platform-request-contract.md` and pass only references:
When Agent is the executor, Manager should create a high-risk `newapi-rebuild-deploy` deployment using `docs/integration/agent-platform-request-contract.md` and pass only references:
| Field | Required reference |
|---|---|
@@ -126,7 +126,7 @@ When Agnet is the executor, Manager should create a high-risk `newapi-rebuild-de
| `orchestration_plan.constraints.healthcheck_url_ref` | `env://NEWAPI_HEALTHCHECK_URL`, expected to resolve to the VM-local `/api/status` probe. |
| `orchestration_plan.metadata.commit` | Intended Git commit or branch to deploy, such as `origin/main` after push. |
Minimum evidence Agnet must return before Manager marks the operation deployed:
Minimum evidence Agent must return before Manager marks the operation deployed:
1. Remote commit after fetch/pull.
2. `docker compose ... ps` status for `heicode`.
@@ -149,14 +149,14 @@ curl -fsS http://127.0.0.1:3000/api/status
Expected health response includes `"success":true`.
Optional Manager / Agnet smoke probe after obtaining a safe admin session cookie without logging it:
Optional Manager / Agent smoke probe after obtaining a safe admin session cookie without logging it:
```bash
AUTH_COOKIE='REDACTED_SESSION_COOKIE' \
BASE_URL='http://127.0.0.1:3000' \
TENANT_ID='tenant_smoke' \
PROJECT_ID='project_smoke' \
./bin/acceptance_agnet_local.sh
./bin/acceptance_agent_local.sh
```
Do not commit or report the real cookie.
+4 -4
View File
@@ -29,13 +29,13 @@ import (
func TestUserOrV2DeviceAuthRejectsMalformedEncryptedRequest(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/api/agnet/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
router.POST("/api/agent/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
})
req := httptest.NewRequest(
http.MethodPost,
"/api/agnet/user/tasks/task-v2/deployment-draft",
"/api/agent/user/tasks/task-v2/deployment-draft",
nil,
)
req.Header.Set("Content-Encoding", V2ContentEncoding)
@@ -100,7 +100,7 @@ func TestUserOrV2DeviceAuthDecryptsValidEncryptedRequest(t *testing.T) {
}
router := gin.New()
router.POST("/api/agnet/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
router.POST("/api/agent/user/tasks/:task_id/deployment-draft", UserOrV2DeviceAuth(), func(c *gin.Context) {
if got := c.GetInt("id"); got != 42 {
t.Fatalf("id context = %d, want 42", got)
}
@@ -116,7 +116,7 @@ func TestUserOrV2DeviceAuthDecryptsValidEncryptedRequest(t *testing.T) {
c.JSON(http.StatusOK, gin.H{"success": true})
})
path := "/api/agnet/user/tasks/task-v2/deployment-draft"
path := "/api/agent/user/tasks/task-v2/deployment-draft"
body := []byte(`{"objective":"encrypted sub task"}`)
req := newEncryptedV2Request(t, http.MethodPost, path, body, deviceID, fingerprint, privateKey)
rec := httptest.NewRecorder()
@@ -1,10 +1,10 @@
package model
// AgnetApprovalRequest records a user-visible approval gate for a
// high-risk Agnet operation. It intentionally stores only a Secret
// AgentApprovalRequest records a user-visible approval gate for a
// high-risk Agent operation. It intentionally stores only a Secret
// Store reference for credential-backed operations; plaintext secrets
// must never be written to this table.
type AgnetApprovalRequest struct {
type AgentApprovalRequest struct {
Id int `json:"id" gorm:"primaryKey"`
ApprovalID string `json:"approval_id" gorm:"type:varchar(64);uniqueIndex;not null"`
UserId int `json:"user_id" gorm:"index;not null"`
@@ -31,14 +31,14 @@ type AgnetApprovalRequest struct {
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
}
func (AgnetApprovalRequest) TableName() string {
return "agnet_approval_requests"
func (AgentApprovalRequest) TableName() string {
return "agent_approval_requests"
}
// AgnetCredentialLease is the Manager-side short-lived credential
// AgentCredentialLease is the Manager-side short-lived credential
// handle produced after an approval succeeds. CredentialRef is the
// external handle; SecretRef is internal and points at Azure Key Vault.
type AgnetCredentialLease struct {
type AgentCredentialLease struct {
Id int `json:"id" gorm:"primaryKey"`
LeaseID string `json:"lease_id" gorm:"type:varchar(64);uniqueIndex;not null"`
CredentialRef string `json:"credential_ref" gorm:"type:varchar(128);uniqueIndex;not null"`
@@ -59,6 +59,6 @@ type AgnetCredentialLease struct {
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
}
func (AgnetCredentialLease) TableName() string {
return "agnet_credential_leases"
func (AgentCredentialLease) TableName() string {
return "agent_credential_leases"
}
@@ -7,7 +7,7 @@ import (
"gorm.io/gorm"
)
type AgnetArtifact struct {
type AgentArtifact struct {
Id int `gorm:"primaryKey" json:"id"`
ArtifactID string `gorm:"type:varchar(128);uniqueIndex" json:"artifact_id"`
DeploymentID string `gorm:"type:varchar(64);index" json:"deployment_id"`
@@ -24,22 +24,22 @@ type AgnetArtifact struct {
CreatedAtMs int64 `gorm:"bigint;index" json:"created_at_ms"`
}
func (AgnetArtifact) TableName() string {
return "agnet_artifacts"
func (AgentArtifact) TableName() string {
return "agent_artifacts"
}
type ListAgnetArtifactsFilter struct {
type ListAgentArtifactsFilter struct {
DeploymentID string
TaskID string
CorrelationID string
Limit int
}
func UpsertAgnetArtifact(row *AgnetArtifact) error {
func UpsertAgentArtifact(row *AgentArtifact) error {
if DB == nil || row == nil {
return nil
}
var existing AgnetArtifact
var existing AgentArtifact
if row.ArtifactID != "" {
if err := DB.Where("artifact_id = ?", row.ArtifactID).First(&existing).Error; err == nil {
row.Id = existing.Id
@@ -49,11 +49,11 @@ func UpsertAgnetArtifact(row *AgnetArtifact) error {
return DB.Create(row).Error
}
func ListAgnetArtifacts(f ListAgnetArtifactsFilter) ([]AgnetArtifact, error) {
func ListAgentArtifacts(f ListAgentArtifactsFilter) ([]AgentArtifact, error) {
if DB == nil {
return nil, nil
}
q := DB.Model(&AgnetArtifact{})
q := DB.Model(&AgentArtifact{})
if f.DeploymentID != "" {
q = q.Where("deployment_id = ?", f.DeploymentID)
}
@@ -67,13 +67,13 @@ func ListAgnetArtifacts(f ListAgnetArtifactsFilter) ([]AgnetArtifact, error) {
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgnetArtifact
var items []AgentArtifact
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
return items, err
}
func GetAgnetArtifactByDeployment(deploymentID string, artifactID string) (AgnetArtifact, bool, error) {
var row AgnetArtifact
func GetAgentArtifactByDeployment(deploymentID string, artifactID string) (AgentArtifact, bool, error) {
var row AgentArtifact
if DB == nil {
return row, false, nil
}
@@ -6,15 +6,15 @@ import (
"github.com/heicode/manager/common"
)
// AgnetAuditEvent is the persistent audit-trail row for the Agnet
// AgentAuditEvent is the persistent audit-trail row for the Agent
// control-plane. Each row records one observable transition in the
// Agnet lifecycle — deployment accepted, instance phase changed, SK
// Agent lifecycle — deployment accepted, instance phase changed, SK
// snapshot refreshed, etc. — so admins can answer the "who / when /
// for which task / against which resource / with what result" set of
// questions even after the Manager container restarts.
//
// Before this table existed the control-plane stashed events in an
// in-process `map[string][]agnetEvent` (controller/agnet_control_plane
// in-process `map[string][]agentEvent` (controller/agent_control_plane
// .go:219). Every restart wiped audit history — unacceptable for a
// product where the audit page is part of the security story.
//
@@ -32,7 +32,7 @@ import (
// Cross-DB compatibility (CLAUDE.md Rule 2): GORM AutoMigrate maps the
// tags to the correct types on SQLite / MySQL / PostgreSQL. No raw
// SQL. No DB-specific column types.
type AgnetAuditEvent struct {
type AgentAuditEvent struct {
Id int `gorm:"primaryKey" json:"id"`
EventID string `gorm:"type:varchar(64);uniqueIndex" json:"event_id"`
Event string `gorm:"type:varchar(64);index" json:"event"`
@@ -52,17 +52,17 @@ type AgnetAuditEvent struct {
// TableName pins the migration target so we can rename the Go struct
// later without breaking the deployed schema.
func (AgnetAuditEvent) TableName() string {
return "agnet_audit_events"
func (AgentAuditEvent) TableName() string {
return "agent_audit_events"
}
// InsertAgnetAuditEvent best-effort persists one audit row. Callers
// InsertAgentAuditEvent best-effort persists one audit row. Callers
// invoke this in a hot path (right after mutating a deployment), so:
// - errors are logged but never returned — the audit write must NOT
// fail the user-facing API
// - DB is nil-guarded so unit tests / partial-init binaries don't
// panic on a missing connection
func InsertAgnetAuditEvent(evt *AgnetAuditEvent) {
func InsertAgentAuditEvent(evt *AgentAuditEvent) {
if DB == nil || evt == nil {
return
}
@@ -76,14 +76,14 @@ func InsertAgnetAuditEvent(evt *AgnetAuditEvent) {
evt.Result = "ok"
}
if err := DB.Create(evt).Error; err != nil {
common.SysLog("InsertAgnetAuditEvent: " + err.Error())
common.SysLog("InsertAgentAuditEvent: " + err.Error())
}
}
// ListAgnetAuditEventsFilter narrows the audit query to a slice of
// ListAgentAuditEventsFilter narrows the audit query to a slice of
// dashboard relevant rows. Zero-value fields are ignored — callers
// pass only the filters they care about.
type ListAgnetAuditEventsFilter struct {
type ListAgentAuditEventsFilter struct {
UserID string
BindingScope string
DeploymentID string
@@ -94,14 +94,14 @@ type ListAgnetAuditEventsFilter struct {
Offset int
}
// ListAgnetAuditEvents pages over audit rows ordered newest-first.
// Used by the /api/agnet/audit-logs endpoint and the future task-
// ListAgentAuditEvents pages over audit rows ordered newest-first.
// Used by the /api/agent/audit-logs endpoint and the future task-
// scoped audit drawer.
func ListAgnetAuditEvents(f ListAgnetAuditEventsFilter) ([]AgnetAuditEvent, int64, error) {
func ListAgentAuditEvents(f ListAgentAuditEventsFilter) ([]AgentAuditEvent, int64, error) {
if DB == nil {
return nil, 0, nil
}
q := DB.Model(&AgnetAuditEvent{})
q := DB.Model(&AgentAuditEvent{})
if f.UserID != "" {
q = q.Where("user_id = ?", f.UserID)
}
@@ -128,19 +128,19 @@ func ListAgnetAuditEvents(f ListAgnetAuditEventsFilter) ([]AgnetAuditEvent, int6
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgnetAuditEvent
var items []AgentAuditEvent
err := q.Order("occurred_at desc, id desc").Limit(limit).Offset(f.Offset).Find(&items).Error
return items, total, err
}
// ListAgnetAuditEventsByDeployment is the hot path for the deployment
// ListAgentAuditEventsByDeployment is the hot path for the deployment
// detail drawer — returns all events for one deployment in chronological
// order so the timeline reads top-to-bottom.
func ListAgnetAuditEventsByDeployment(deploymentID string) ([]AgnetAuditEvent, error) {
func ListAgentAuditEventsByDeployment(deploymentID string) ([]AgentAuditEvent, error) {
if DB == nil || deploymentID == "" {
return nil, nil
}
var items []AgnetAuditEvent
var items []AgentAuditEvent
err := DB.Where("deployment_id = ?", deploymentID).
Order("occurred_at asc, id asc").
Find(&items).Error
@@ -15,24 +15,24 @@ import (
func setupAuditTest(t *testing.T) {
t.Helper()
if err := DB.AutoMigrate(&AgnetAuditEvent{}); err != nil {
if err := DB.AutoMigrate(&AgentAuditEvent{}); err != nil {
t.Fatalf("migrate: %v", err)
}
if err := DB.Exec("DELETE FROM agnet_audit_events").Error; err != nil {
if err := DB.Exec("DELETE FROM agent_audit_events").Error; err != nil {
t.Fatalf("truncate: %v", err)
}
}
func TestInsertAgnetAuditEvent_PersistsRow(t *testing.T) {
func TestInsertAgentAuditEvent_PersistsRow(t *testing.T) {
// Regression for H1 — the previous implementation appended to an
// in-process map that was wiped on every container restart. This
// test pins the new behaviour: rows survive in DB.
setupAuditTest(t)
evt := &AgnetAuditEvent{
evt := &AgentAuditEvent{
EventID: "evt_test_001",
Event: "deployment.accepted",
Actor: "agnet_control_plane",
Actor: "agent_control_plane",
Resource: "dep_abc",
UserID: "user-42",
ChannelID: "channel-1",
@@ -41,9 +41,9 @@ func TestInsertAgnetAuditEvent_PersistsRow(t *testing.T) {
CorrelationID: "corr-xyz",
RequestID: "req-001",
}
InsertAgnetAuditEvent(evt)
InsertAgentAuditEvent(evt)
var got AgnetAuditEvent
var got AgentAuditEvent
if err := DB.Where("event_id = ?", "evt_test_001").First(&got).Error; err != nil {
t.Fatalf("not persisted: %v", err)
}
@@ -58,24 +58,24 @@ func TestInsertAgnetAuditEvent_PersistsRow(t *testing.T) {
}
}
func TestInsertAgnetAuditEvent_NilGuards(t *testing.T) {
func TestInsertAgentAuditEvent_NilGuards(t *testing.T) {
// Production safety: audit writes run inside hot paths (right
// after a deployment mutation). A nil DB or nil event MUST NOT
// panic — better to drop the audit row than to fail the user
// API call.
defer func() {
if r := recover(); r != nil {
t.Fatalf("InsertAgnetAuditEvent panicked: %v", r)
t.Fatalf("InsertAgentAuditEvent panicked: %v", r)
}
}()
InsertAgnetAuditEvent(nil) // nil evt
InsertAgentAuditEvent(nil) // nil evt
prev := DB
DB = nil
InsertAgnetAuditEvent(&AgnetAuditEvent{EventID: "x"}) // nil DB
InsertAgentAuditEvent(&AgentAuditEvent{EventID: "x"}) // nil DB
DB = prev
}
func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
func TestListAgentAuditEvents_FilterAndPaginate(t *testing.T) {
setupAuditTest(t)
now := time.Now().UnixMilli()
@@ -84,7 +84,7 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
if i%2 == 0 {
uid = "user-B"
}
InsertAgnetAuditEvent(&AgnetAuditEvent{
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: fmt.Sprintf("evt_%d", i),
Event: "deployment.accepted",
UserID: uid,
@@ -94,7 +94,7 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
}
// All rows visible without filter.
rows, total, err := ListAgnetAuditEvents(ListAgnetAuditEventsFilter{})
rows, total, err := ListAgentAuditEvents(ListAgentAuditEventsFilter{})
if err != nil {
t.Fatalf("list: %v", err)
}
@@ -103,7 +103,7 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
}
// user_id filter narrows to 3 (i=0,2,4 → user-B).
_, total, err = ListAgnetAuditEvents(ListAgnetAuditEventsFilter{UserID: "user-B"})
_, total, err = ListAgentAuditEvents(ListAgentAuditEventsFilter{UserID: "user-B"})
if err != nil {
t.Fatalf("list with userid: %v", err)
}
@@ -112,28 +112,28 @@ func TestListAgnetAuditEvents_FilterAndPaginate(t *testing.T) {
}
// Newest-first ordering. evt_4 inserted last → top.
rows, _, _ = ListAgnetAuditEvents(ListAgnetAuditEventsFilter{})
rows, _, _ = ListAgentAuditEvents(ListAgentAuditEventsFilter{})
if rows[0].EventID != "evt_4" {
t.Errorf("expected newest-first, got %q on top", rows[0].EventID)
}
}
func TestListAgnetAuditEventsByDeployment_Chronological(t *testing.T) {
func TestListAgentAuditEventsByDeployment_Chronological(t *testing.T) {
// Detail-drawer reads need oldest-first for a top-to-bottom
// timeline. Confirms ascending order independent of insertion
// order.
setupAuditTest(t)
InsertAgnetAuditEvent(&AgnetAuditEvent{
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: "evt_late", Event: "x", DeploymentID: "dep_T",
OccurredAt: 9000,
})
InsertAgnetAuditEvent(&AgnetAuditEvent{
InsertAgentAuditEvent(&AgentAuditEvent{
EventID: "evt_early", Event: "x", DeploymentID: "dep_T",
OccurredAt: 1000,
})
rows, err := ListAgnetAuditEventsByDeployment("dep_T")
rows, err := ListAgentAuditEventsByDeployment("dep_T")
if err != nil {
t.Fatalf("query: %v", err)
}
@@ -2,7 +2,7 @@ package model
import "errors"
type AgnetCallbackEvent struct {
type AgentCallbackEvent struct {
Id int `gorm:"primaryKey" json:"id"`
EventID string `gorm:"type:varchar(128);uniqueIndex" json:"event_id"`
IdempotencyKey string `gorm:"type:varchar(128);index" json:"idempotency_key"`
@@ -22,25 +22,25 @@ type AgnetCallbackEvent struct {
CreatedAtMs int64 `gorm:"bigint;index" json:"created_at_ms"`
}
func (AgnetCallbackEvent) TableName() string {
return "agnet_callback_events"
func (AgentCallbackEvent) TableName() string {
return "agent_callback_events"
}
type ListAgnetCallbackEventsFilter struct {
type ListAgentCallbackEventsFilter struct {
DeploymentID string
TaskID string
CorrelationID string
Limit int
}
func InsertAgnetCallbackEvent(row *AgnetCallbackEvent) (bool, error) {
func InsertAgentCallbackEvent(row *AgentCallbackEvent) (bool, error) {
if DB == nil || row == nil {
return false, nil
}
if row.EventID == "" {
return false, errors.New("event_id is required")
}
var existing AgnetCallbackEvent
var existing AgentCallbackEvent
if err := DB.Where("event_id = ?", row.EventID).First(&existing).Error; err == nil {
return false, nil
}
@@ -55,11 +55,11 @@ func InsertAgnetCallbackEvent(row *AgnetCallbackEvent) (bool, error) {
return true, nil
}
func ListAgnetCallbackEvents(f ListAgnetCallbackEventsFilter) ([]AgnetCallbackEvent, error) {
func ListAgentCallbackEvents(f ListAgentCallbackEventsFilter) ([]AgentCallbackEvent, error) {
if DB == nil {
return nil, nil
}
q := DB.Model(&AgnetCallbackEvent{})
q := DB.Model(&AgentCallbackEvent{})
if f.DeploymentID != "" {
q = q.Where("deployment_id = ?", f.DeploymentID)
}
@@ -73,7 +73,7 @@ func ListAgnetCallbackEvents(f ListAgnetCallbackEventsFilter) ([]AgnetCallbackEv
if limit <= 0 || limit > 1000 {
limit = 200
}
var items []AgnetCallbackEvent
var items []AgentCallbackEvent
err := q.Order("created_at_ms asc, id asc").Limit(limit).Find(&items).Error
return items, err
}
@@ -1,10 +1,10 @@
package model
// AgnetDeployment stores the Manager-side deployment placeholder.
// It is intentionally a control-plane snapshot: the real Agnet platform
// AgentDeployment stores the Manager-side deployment placeholder.
// It is intentionally a control-plane snapshot: the real Agent platform
// execution state can attach later, but Manager must not lose the accepted
// plan, manifest, status, or audit context across container restarts.
type AgnetDeployment struct {
type AgentDeployment struct {
Id int `gorm:"primaryKey" json:"id"`
DeploymentID string `gorm:"type:varchar(64);uniqueIndex" json:"deployment_id"`
UserID string `gorm:"type:varchar(64);index" json:"user_id"`
@@ -29,6 +29,6 @@ type AgnetDeployment struct {
PayloadJSON string `gorm:"type:text" json:"payload_json"`
}
func (AgnetDeployment) TableName() string {
return "agnet_deployments"
func (AgentDeployment) TableName() string {
return "agent_deployments"
}
@@ -1,6 +1,6 @@
package model
type AgnetSKSnapshot struct {
type AgentSKSnapshot struct {
Id int `gorm:"primaryKey" json:"id"`
SnapshotID string `gorm:"type:varchar(128);uniqueIndex" json:"snapshot_id"`
DeploymentID string `gorm:"type:varchar(64);index" json:"deployment_id"`
@@ -12,22 +12,22 @@ type AgnetSKSnapshot struct {
ResolvedAtMs int64 `gorm:"bigint;index" json:"resolved_at_ms"`
}
func (AgnetSKSnapshot) TableName() string {
return "agnet_sk_snapshots"
func (AgentSKSnapshot) TableName() string {
return "agent_sk_snapshots"
}
func InsertAgnetSKSnapshots(items []AgnetSKSnapshot) error {
func InsertAgentSKSnapshots(items []AgentSKSnapshot) error {
if DB == nil || len(items) == 0 {
return nil
}
return DB.Create(&items).Error
}
func ListAgnetSKSnapshots(deploymentID string) ([]AgnetSKSnapshot, error) {
func ListAgentSKSnapshots(deploymentID string) ([]AgentSKSnapshot, error) {
if DB == nil || deploymentID == "" {
return nil, nil
}
var items []AgnetSKSnapshot
var items []AgentSKSnapshot
err := DB.Where("deployment_id = ?", deploymentID).
Order("resolved_at_ms asc, id asc").
Find(&items).Error
+53 -17
View File
@@ -247,6 +247,40 @@ func InitLogDB() (err error) {
return err
}
// renameAgnetTablesToAgent renames legacy agnet_* tables (and the
// resource_grants.agnet_id column) to their agent_* equivalents, preserving
// existing production data after the agnet->agent terminology unification.
// Idempotent and cross-DB (uses GORM Migrator). Runs before AutoMigrate so the
// renamed tables are reused instead of being recreated empty.
func renameAgnetTablesToAgent() {
m := DB.Migrator()
pairs := [][2]string{
{"agnet_approval_requests", "agent_approval_requests"},
{"agnet_credential_leases", "agent_credential_leases"},
{"agnet_deployments", "agent_deployments"},
{"agnet_callback_events", "agent_callback_events"},
{"agnet_artifacts", "agent_artifacts"},
{"agnet_sk_snapshots", "agent_sk_snapshots"},
{"agnet_audit_events", "agent_audit_events"},
}
for _, p := range pairs {
if m.HasTable(p[0]) && !m.HasTable(p[1]) {
if err := m.RenameTable(p[0], p[1]); err != nil {
common.SysLog("renameAgnetTablesToAgent: rename " + p[0] + " -> " + p[1] + ": " + err.Error())
} else {
common.SysLog("renameAgnetTablesToAgent: renamed " + p[0] + " -> " + p[1])
}
}
}
if m.HasTable("resource_grants") && m.HasColumn(&ResourceGrant{}, "agnet_id") && !m.HasColumn(&ResourceGrant{}, "agent_id") {
if err := m.RenameColumn(&ResourceGrant{}, "agnet_id", "agent_id"); err != nil {
common.SysLog("renameAgnetTablesToAgent: rename column agnet_id -> agent_id: " + err.Error())
} else {
common.SysLog("renameAgnetTablesToAgent: renamed column resource_grants.agnet_id -> agent_id")
}
}
}
func migrateDB() error {
// Migrate price_amount column from float/double to decimal for existing tables
migrateSubscriptionPlanPriceAmount()
@@ -254,6 +288,8 @@ func migrateDB() error {
if err := migrateTokenModelLimitsToText(); err != nil {
return err
}
// Preserve data across the agnet->agent rename (idempotent, runs before AutoMigrate).
renameAgnetTablesToAgent()
err := DB.AutoMigrate(
&Channel{},
@@ -283,19 +319,19 @@ func migrateDB() error {
&GitSource{},
&ResourceBinding{},
&ResourceGrant{},
&AgnetApprovalRequest{},
&AgnetCredentialLease{},
&AgnetDeployment{},
&AgnetCallbackEvent{},
&AgnetArtifact{},
&AgnetSKSnapshot{},
&AgentApprovalRequest{},
&AgentCredentialLease{},
&AgentDeployment{},
&AgentCallbackEvent{},
&AgentArtifact{},
&AgentSKSnapshot{},
// V2 device-binding: X25519 keypair the Manager uses for ECDH
// body decryption. See model/server_key.go.
&ServerKey{},
// Agnet control-plane audit trail. Replaces the previous
// in-process `agnetEvents map` that was wiped on every container
// restart. See model/agnet_audit.go for the rationale.
&AgnetAuditEvent{},
// Agent control-plane audit trail. Replaces the previous
// in-process `agentEvents map` that was wiped on every container
// restart. See model/agent_audit.go for the rationale.
&AgentAuditEvent{},
)
if err != nil {
return err
@@ -355,13 +391,13 @@ func migrateDBFast() error {
{&GitSource{}, "GitSource"},
{&ResourceBinding{}, "ResourceBinding"},
{&ResourceGrant{}, "ResourceGrant"},
{&AgnetApprovalRequest{}, "AgnetApprovalRequest"},
{&AgnetCredentialLease{}, "AgnetCredentialLease"},
{&AgnetDeployment{}, "AgnetDeployment"},
{&AgnetCallbackEvent{}, "AgnetCallbackEvent"},
{&AgnetArtifact{}, "AgnetArtifact"},
{&AgnetSKSnapshot{}, "AgnetSKSnapshot"},
{&AgnetAuditEvent{}, "AgnetAuditEvent"},
{&AgentApprovalRequest{}, "AgentApprovalRequest"},
{&AgentCredentialLease{}, "AgentCredentialLease"},
{&AgentDeployment{}, "AgentDeployment"},
{&AgentCallbackEvent{}, "AgentCallbackEvent"},
{&AgentArtifact{}, "AgentArtifact"},
{&AgentSKSnapshot{}, "AgentSKSnapshot"},
{&AgentAuditEvent{}, "AgentAuditEvent"},
}
// 动态计算migration数量,确保errChan缓冲区足够大
errChan := make(chan error, len(migrations))
+2 -2
View File
@@ -22,7 +22,7 @@ type ResourceBinding struct {
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
}
// ResourceGrant assigns a ResourceBinding to a role and child Agnet.
// ResourceGrant assigns a ResourceBinding to a role and child Agent.
// It is the auditable Manager expression of "user grants bound resource to role".
type ResourceGrant struct {
Id int `json:"id"`
@@ -32,7 +32,7 @@ type ResourceGrant struct {
BindingScope string `json:"binding_scope" gorm:"type:varchar(512);index"`
ResourceId int `json:"resource_id" gorm:"index;not null"`
Role string `json:"role" gorm:"type:varchar(128);index;not null"`
AgnetId string `json:"agnet_id" gorm:"type:varchar(128);index;not null"`
AgentId string `json:"agent_id" gorm:"type:varchar(128);index;not null"`
PermissionScope string `json:"permission_scope" gorm:"type:text"`
Constraints string `json:"constraints" gorm:"type:text"`
Status string `json:"status" gorm:"type:varchar(32);default:'active';index"`
@@ -21,7 +21,7 @@ import (
"gorm.io/gorm"
)
func setupAgnetRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
func setupAgentRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
t.Helper()
common.UsingSQLite = true
common.UsingMySQL = false
@@ -35,8 +35,8 @@ func setupAgnetRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
model.LOG_DB = db
require.NoError(t, db.AutoMigrate(
&model.User{},
&model.AgnetDeployment{},
&model.AgnetAuditEvent{},
&model.AgentDeployment{},
&model.AgentAuditEvent{},
))
adminToken := "runtime-smoke-admin-token"
require.NoError(t, db.Create(&model.User{
@@ -64,7 +64,7 @@ func setupAgnetRuntimeHTTPSmokeDB(t *testing.T) *gorm.DB {
return db
}
func startAgnetRuntimeManagerSmokeServer(t *testing.T) string {
func startAgentRuntimeManagerSmokeServer(t *testing.T) string {
t.Helper()
gin.SetMode(gin.TestMode)
engine := gin.New()
@@ -82,7 +82,7 @@ func startAgnetRuntimeManagerSmokeServer(t *testing.T) string {
return "http://" + listener.Addr().String()
}
func agnetRuntimeAdminRequest(t *testing.T, method string, url string, body string) *http.Response {
func agentRuntimeAdminRequest(t *testing.T, method string, url string, body string) *http.Response {
t.Helper()
req, err := http.NewRequest(method, url, bytes.NewBufferString(body))
require.NoError(t, err)
@@ -96,7 +96,7 @@ func agnetRuntimeAdminRequest(t *testing.T, method string, url string, body stri
return resp
}
func readAgnetRuntimeSmokeBody(t *testing.T, resp *http.Response) string {
func readAgentRuntimeSmokeBody(t *testing.T, resp *http.Response) string {
t.Helper()
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
@@ -104,16 +104,16 @@ func readAgnetRuntimeSmokeBody(t *testing.T, resp *http.Response) string {
return string(body)
}
func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
db := setupAgnetRuntimeHTTPSmokeDB(t)
func TestAgentRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
db := setupAgentRuntimeHTTPSmokeDB(t)
runtimeCreateCalled := false
runtime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/agnet/health":
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/health":
require.Equal(t, "Bearer runtime-service-token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"healthy"}}`))
case r.Method == http.MethodPost && r.URL.Path == "/api/agnet/deployments":
case r.Method == http.MethodPost && r.URL.Path == "/api/agent/sub-agile/deployments":
runtimeCreateCalled = true
require.Equal(t, "Bearer runtime-service-token", r.Header.Get("Authorization"))
require.Equal(t, "corr-http-smoke", r.Header.Get("X-Correlation-ID"))
@@ -142,23 +142,23 @@ func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
}))
defer runtime.Close()
t.Setenv("AGNET_RUNTIME_ENABLED", "true")
t.Setenv("AGNET_RUNTIME_ASYNC", "false")
t.Setenv("AGNET_RUNTIME_BASE_URL", runtime.URL)
t.Setenv("AGNET_RUNTIME_SERVICE_TOKEN", "runtime-service-token")
t.Setenv("AGNET_RUNTIME_CALLBACK_SIGNING_SECRET_REF", "azkv://heicode-kv.vault.azure.net/secrets/callback-signing")
t.Setenv("AGENT_RUNTIME_ENABLED", "true")
t.Setenv("AGENT_RUNTIME_ASYNC", "false")
t.Setenv("AGENT_RUNTIME_BASE_URL", runtime.URL)
t.Setenv("AGENT_RUNTIME_SERVICE_TOKEN", "runtime-service-token")
t.Setenv("AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF", "azkv://heicode-kv.vault.azure.net/secrets/callback-signing")
managerURL := startAgnetRuntimeManagerSmokeServer(t)
managerURL := startAgentRuntimeManagerSmokeServer(t)
healthResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/runtime/health", "")
healthBody := readAgnetRuntimeSmokeBody(t, healthResp)
healthResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/runtime/health", "")
healthBody := readAgentRuntimeSmokeBody(t, healthResp)
require.Equal(t, http.StatusOK, healthResp.StatusCode)
require.Contains(t, healthBody, `"success":true`)
require.Contains(t, healthBody, `"status":"healthy"`)
schemaResp, err := http.Get(managerURL + "/api/agnet/callbacks/swarm-events/schema")
schemaResp, err := http.Get(managerURL + "/api/agent/callbacks/runtime-events/schema")
require.NoError(t, err)
schemaBody := readAgnetRuntimeSmokeBody(t, schemaResp)
schemaBody := readAgentRuntimeSmokeBody(t, schemaResp)
require.Equal(t, http.StatusOK, schemaResp.StatusCode)
require.Contains(t, schemaBody, `"event_type":"task.claimed"`)
require.Contains(t, schemaBody, `"event_type":"approval.requested"`)
@@ -179,44 +179,44 @@ func TestAgnetRuntimeRealHTTPHealthAndShadowCreateSmoke(t *testing.T) {
"user_context":{"user_id":"101","channel_id":"default"},
"billing_context":{"provider":"newapi","newapi_user_ref":"newapi-http-smoke"},
"agile_context":{"iteration":"2026-05-27~2026-05-28","stage":"development","checkpoint":"ready_for_test","acceptance_criteria":["接口返回成功"],"next_action":"submit_test_result","requires_user_approval":false},
"agent_runtime":{"platform":"agnet","agents":[{"role":"builder","model_ref":"model-http-smoke","instance_count":1}]},
"agent_runtime":{"platform":"agent","agents":[{"role":"builder","model_ref":"model-http-smoke","instance_count":1}]},
"agents":[{"role_template":"builder","goal":"smoke","default_model_id":"model-http-smoke","resource_grants":[{"grant_id":"grant-http-git","resource_id":"git-http","resource_type":"git","user_id":"101","binding_scope":"https://example.invalid/heicode/smoke.git#main","target_role":"builder","target_agent_ref":"agent-builder-1","permission_scope":["repo:read"],"metadata":{"repo_url":"https://example.invalid/heicode/smoke.git"},"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/http-smoke-git","status":"active"}]}],
"constraints":{"allowed_model_ids":["model-http-smoke"]},
"metadata":{"correlation_id":"corr-http-smoke"}
}
}`
createResp := agnetRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agnet/deployments", createBody)
responseBody := readAgnetRuntimeSmokeBody(t, createResp)
createResp := agentRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agent/deployments", createBody)
responseBody := readAgentRuntimeSmokeBody(t, createResp)
require.Equal(t, http.StatusOK, createResp.StatusCode)
require.Contains(t, responseBody, `"success":true`)
require.Contains(t, responseBody, `"runtime_deployment_id":"runtime-http-dep"`)
require.Contains(t, responseBody, `"runtime_swarm_id":"runtime-http-swarm"`)
require.True(t, runtimeCreateCalled)
var stored model.AgnetDeployment
var stored model.AgentDeployment
require.NoError(t, db.Where("runtime_swarm_id = ?", "runtime-http-swarm").First(&stored).Error)
require.Equal(t, "accepted", stored.RuntimeState)
logsResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/deployments/"+stored.DeploymentID+"/logs", "")
logsBody := readAgnetRuntimeSmokeBody(t, logsResp)
logsResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/deployments/"+stored.DeploymentID+"/logs", "")
logsBody := readAgentRuntimeSmokeBody(t, logsResp)
require.Equal(t, http.StatusOK, logsResp.StatusCode)
require.Contains(t, logsBody, `"data_source":"manager_control_plane"`)
require.Contains(t, logsBody, `"runtime_source":"not_connected"`)
metricsResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/deployments/"+stored.DeploymentID+"/metrics", "")
metricsBody := readAgnetRuntimeSmokeBody(t, metricsResp)
metricsResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/deployments/"+stored.DeploymentID+"/metrics", "")
metricsBody := readAgentRuntimeSmokeBody(t, metricsResp)
require.Equal(t, http.StatusOK, metricsResp.StatusCode)
require.Contains(t, metricsBody, `"data_source":"manager_control_plane"`)
require.Contains(t, metricsBody, `"platform_estimated":true`)
}
func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
db := setupAgnetRuntimeHTTPSmokeDB(t)
db := setupAgentRuntimeHTTPSmokeDB(t)
swarmCreateCalled := false
swarmStopCalled := false
swarm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/agnet/health":
case r.Method == http.MethodGet && r.URL.Path == "/api/agent/health":
require.Equal(t, "Bearer swarm-runtime-token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"healthy","service":"heicode-swarm-runtime"}}`))
@@ -253,16 +253,16 @@ func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
}))
defer swarm.Close()
t.Setenv("AGNET_RUNTIME_ENABLED", "false")
t.Setenv("AGENT_RUNTIME_ENABLED", "false")
t.Setenv("SWARM_RUNTIME_ENABLED", "true")
t.Setenv("SWARM_RUNTIME_ASYNC", "false")
t.Setenv("SWARM_RUNTIME_BASE_URL", swarm.URL)
t.Setenv("SWARM_RUNTIME_SERVICE_TOKEN", "swarm-runtime-token")
managerURL := startAgnetRuntimeManagerSmokeServer(t)
managerURL := startAgentRuntimeManagerSmokeServer(t)
healthResp := agnetRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agnet/runtime/health?mode=swarm", "")
healthBody := readAgnetRuntimeSmokeBody(t, healthResp)
healthResp := agentRuntimeAdminRequest(t, http.MethodGet, managerURL+"/api/agent/runtime/health?mode=swarm", "")
healthBody := readAgentRuntimeSmokeBody(t, healthResp)
require.Equal(t, http.StatusOK, healthResp.StatusCode)
require.Contains(t, healthBody, `"mode":"swarm"`)
require.Contains(t, healthBody, `"status":"healthy"`)
@@ -279,7 +279,7 @@ func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
"user_context":{"user_id":"101","channel_id":"default"},
"billing_context":{"provider":"newapi","newapi_user_ref":"newapi-swarm-smoke"},
"agile_context":{"iteration":"2026-05-29","stage":"testing","checkpoint":"runtime_accepted","acceptance_criteria":["Runtime creates a swarm run"],"next_action":"submit_test_result","requires_user_approval":false},
"agent_runtime":{"platform":"agnet","agents":[{"role":"planner","model_ref":"model-swarm","instance_count":1},{"role":"builder","model_ref":"model-swarm","instance_count":1},{"role":"reviewer","model_ref":"model-swarm","instance_count":1}]},
"agent_runtime":{"platform":"agent","agents":[{"role":"planner","model_ref":"model-swarm","instance_count":1},{"role":"builder","model_ref":"model-swarm","instance_count":1},{"role":"reviewer","model_ref":"model-swarm","instance_count":1}]},
"agents":[
{"role_template":"planner","goal":"plan the swarm task","default_model_id":"model-swarm","resource_grants":[{"grant_id":"grant-swarm-plan","resource_id":"doc-swarm","resource_type":"project_doc","user_id":"101","binding_scope":"task-swarm-smoke","target_role":"planner","target_agent_ref":"agent-planner-1","permission_scope":["doc:read"],"metadata":{"resource_ref":"task-swarm-smoke"},"status":"active"}]},
{"role_template":"builder","goal":"build the swarm output","default_model_id":"model-swarm","resource_grants":[{"grant_id":"grant-swarm-build","resource_id":"git-swarm","resource_type":"git","user_id":"101","binding_scope":"task-swarm-smoke","target_role":"builder","target_agent_ref":"agent-builder-1","permission_scope":["repo:read"],"metadata":{"repo_url":"https://example.invalid/heicode/swarm.git"},"secret_ref":"azkv://heicode-kv.vault.azure.net/secrets/swarm-git","status":"active"}]},
@@ -289,21 +289,21 @@ func TestSwarmRuntimeHTTPCreateUsesSwarmConfigAndPayload(t *testing.T) {
"metadata":{"correlation_id":"corr-swarm-smoke"}
}
}`
createResp := agnetRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/swarms", createBody)
responseBody := readAgnetRuntimeSmokeBody(t, createResp)
createResp := agentRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/swarms", createBody)
responseBody := readAgentRuntimeSmokeBody(t, createResp)
require.Equal(t, http.StatusOK, createResp.StatusCode)
require.Contains(t, responseBody, `"success":true`)
require.Contains(t, responseBody, `"runtime_deployment_id":"swarm-runtime-dep"`)
require.Contains(t, responseBody, `"runtime_swarm_id":"swarm-runtime-id"`)
require.True(t, swarmCreateCalled)
var stored model.AgnetDeployment
var stored model.AgentDeployment
require.NoError(t, db.Where("runtime_swarm_id = ?", "swarm-runtime-id").First(&stored).Error)
require.Equal(t, "agile", stored.SubMode)
require.Contains(t, stored.PlanJSON, `"runtime_mode":"swarm"`)
stopResp := agnetRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agnet/user/deployments/"+stored.DeploymentID+"/stop", `{"reason":"smoke done"}`)
stopBody := readAgnetRuntimeSmokeBody(t, stopResp)
stopResp := agentRuntimeAdminRequest(t, http.MethodPost, managerURL+"/api/agent/user/deployments/"+stored.DeploymentID+"/stop", `{"reason":"smoke done"}`)
stopBody := readAgentRuntimeSmokeBody(t, stopResp)
require.Equal(t, http.StatusOK, stopResp.StatusCode)
require.Contains(t, stopBody, `"success":true`)
require.Contains(t, stopBody, `"runtime_state":"stopped"`)
+76 -44
View File
@@ -59,9 +59,11 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
apiRouter.GET("/agnet/callbacks/swarm-events/schema", controller.AgnetGetSwarmEventCallbackSchema)
apiRouter.POST("/agnet/callbacks/swarm-events", controller.AgnetReceiveSwarmEventCallback)
apiRouter.POST("/swarms", middleware.UserOrV2DeviceAuth(), controller.AgnetCreateUserSwarm)
apiRouter.GET("/agent/callbacks/runtime-events/schema", controller.AgentGetRuntimeEventCallbackSchema)
apiRouter.POST("/agent/callbacks/runtime-events", controller.AgentReceiveRuntimeEventCallback)
// Client-facing capability discovery (unified spec §6). Catalog data only.
apiRouter.GET("/heicode/capabilities", controller.HeicodeCapabilities)
apiRouter.POST("/swarms", middleware.UserOrV2DeviceAuth(), controller.AgentCreateUserSwarm)
//apiRouter.POST("/waffo-pancake/webhook", controller.WaffoPancakeWebhook)
// Universal secure verification routes
@@ -81,7 +83,7 @@ func SetApiRouter(router *gin.Engine) {
userRoute := apiRouter.Group("/user")
{
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
userRoute.POST("/session/from-agnet", middleware.CriticalRateLimit(), controller.HeicodeAgnetSessionLogin)
userRoute.POST("/session/from-agent", middleware.CriticalRateLimit(), controller.HeicodeAgentSessionLogin)
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login)
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), controller.Verify2FALogin)
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), controller.PasskeyLoginBegin)
@@ -491,50 +493,80 @@ func SetApiRouter(router *gin.Engine) {
deploymentsRoute.DELETE("/:id", controller.DeleteDeployment)
}
// Agnet user approval gates and short-lived credential leases.
agnetApprovalRoute := apiRouter.Group("/agnet")
agnetApprovalRoute.Use(middleware.UserOrV2DeviceAuth())
// Agent user approval gates and short-lived credential leases.
agentApprovalRoute := apiRouter.Group("/agent")
agentApprovalRoute.Use(middleware.UserOrV2DeviceAuth())
{
agnetApprovalRoute.GET("/approvals", controller.ListAgnetApprovalRequests)
agnetApprovalRoute.POST("/approvals", controller.CreateAgnetApprovalRequest)
agnetApprovalRoute.GET("/approvals/:approval_id", controller.GetAgnetApprovalRequest)
agnetApprovalRoute.POST("/approvals/:approval_id/approve", controller.ApproveAgnetApprovalRequest)
agnetApprovalRoute.POST("/approvals/:approval_id/reject", controller.RejectAgnetApprovalRequest)
agnetApprovalRoute.GET("/credential-leases", controller.ListAgnetCredentialLeases)
agnetApprovalRoute.POST("/credential-leases/:lease_id/revoke", controller.RevokeAgnetCredentialLease)
agnetApprovalRoute.GET("/role-templates", controller.AgnetListRoleTemplates)
agnetApprovalRoute.GET("/user/deployments", controller.AgnetListUserDeployments)
agnetApprovalRoute.POST("/user/deployments", controller.AgnetCreateUserDeployment)
agnetApprovalRoute.GET("/user/deployments/:deployment_id", controller.AgnetGetUserDeployment)
agnetApprovalRoute.POST("/user/deployments/:deployment_id/stop", controller.AgnetStopUserDeployment)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/logs", controller.AgnetListUserDeploymentLogs)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/metrics", controller.AgnetGetUserDeploymentMetrics)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/runtime-diagnostics", controller.AgnetGetUserDeploymentRuntimeDiagnostics)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/events", controller.AgnetListUserDeploymentEvents)
agnetApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgnetSimulateUserDeploymentEvents)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts", controller.AgnetListUserDeploymentArtifacts)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/artifacts/:artifact_id/content", controller.AgnetGetUserDeploymentArtifactContent)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/sk-snapshots", controller.AgnetListUserSKSnapshots)
agnetApprovalRoute.GET("/user/deployments/:deployment_id/timeline", controller.AgnetGetUserDeploymentTimeline)
agnetApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgnetCreateTaskDeploymentDraft)
agentApprovalRoute.GET("/approvals", controller.ListAgentApprovalRequests)
agentApprovalRoute.POST("/approvals", controller.CreateAgentApprovalRequest)
agentApprovalRoute.GET("/approvals/:approval_id", controller.GetAgentApprovalRequest)
agentApprovalRoute.POST("/approvals/:approval_id/approve", controller.ApproveAgentApprovalRequest)
agentApprovalRoute.POST("/approvals/:approval_id/reject", controller.RejectAgentApprovalRequest)
agentApprovalRoute.GET("/credential-leases", controller.ListAgentCredentialLeases)
agentApprovalRoute.POST("/credential-leases/:lease_id/revoke", controller.RevokeAgentCredentialLease)
agentApprovalRoute.GET("/role-templates", controller.AgentListRoleTemplates)
agentApprovalRoute.GET("/user/deployments", controller.AgentListUserDeployments)
agentApprovalRoute.POST("/user/deployments", controller.AgentCreateUserDeployment)
agentApprovalRoute.GET("/user/deployments/:deployment_id", controller.AgentGetUserDeployment)
agentApprovalRoute.POST("/user/deployments/:deployment_id/stop", controller.AgentStopUserDeployment)
agentApprovalRoute.GET("/user/deployments/:deployment_id/logs", controller.AgentListUserDeploymentLogs)
agentApprovalRoute.GET("/user/deployments/:deployment_id/metrics", controller.AgentGetUserDeploymentMetrics)
agentApprovalRoute.GET("/user/deployments/:deployment_id/runtime-diagnostics", controller.AgentGetUserDeploymentRuntimeDiagnostics)
agentApprovalRoute.GET("/user/deployments/:deployment_id/events", controller.AgentListUserDeploymentEvents)
agentApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgentSimulateUserDeploymentEvents)
agentApprovalRoute.GET("/user/deployments/:deployment_id/artifacts", controller.AgentListUserDeploymentArtifacts)
agentApprovalRoute.GET("/user/deployments/:deployment_id/artifacts/:artifact_id/content", controller.AgentGetUserDeploymentArtifactContent)
agentApprovalRoute.GET("/user/deployments/:deployment_id/sk-snapshots", controller.AgentListUserSKSnapshots)
agentApprovalRoute.GET("/user/deployments/:deployment_id/timeline", controller.AgentGetUserDeploymentTimeline)
agentApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgentCreateTaskDeploymentDraft)
}
// Agnet orchestration control plane (minimal integration endpoints)
agnetRoute := apiRouter.Group("/agnet")
agnetRoute.Use(middleware.AdminAuth())
// Client-facing unified task routes (unified spec §5.1). task_id == the
// deployment_id, so these reuse the deployment control-plane handlers.
// Sub Agile -> agent_management; Swarm -> HeiCode-Swarm (mode is carried
// by the deployment record created at POST /tasks).
registerHeicodeTaskRoutes := func(group *gin.RouterGroup, createHandler gin.HandlerFunc) {
group.GET("/tasks", controller.AgentListUserDeployments)
group.POST("/tasks", createHandler)
group.GET("/tasks/:deployment_id", controller.AgentGetUserDeployment)
group.POST("/tasks/:deployment_id/stop", controller.AgentStopUserDeployment)
group.GET("/tasks/:deployment_id/timeline", controller.AgentGetUserDeploymentTimeline)
group.GET("/tasks/:deployment_id/workflow", controller.HeicodeTaskWorkflow)
group.GET("/tasks/:deployment_id/logs", controller.AgentListUserDeploymentLogs)
group.GET("/tasks/:deployment_id/events", controller.AgentListUserDeploymentEvents)
group.GET("/tasks/:deployment_id/metrics", controller.AgentGetUserDeploymentMetrics)
group.GET("/tasks/:deployment_id/diagnostics", controller.AgentGetUserDeploymentRuntimeDiagnostics)
group.GET("/tasks/:deployment_id/artifacts", controller.AgentListUserDeploymentArtifacts)
group.GET("/tasks/:deployment_id/artifacts/:artifact_id/content", controller.AgentGetUserDeploymentArtifactContent)
group.GET("/tasks/:deployment_id/sk-snapshots", controller.AgentListUserSKSnapshots)
group.POST("/tasks/:deployment_id/approvals/:approval_id/approve", controller.ApproveAgentApprovalRequest)
group.POST("/tasks/:deployment_id/approvals/:approval_id/reject", controller.RejectAgentApprovalRequest)
}
heicodeSubAgileRoute := apiRouter.Group("/heicode/sub-agile")
heicodeSubAgileRoute.Use(middleware.UserOrV2DeviceAuth())
registerHeicodeTaskRoutes(heicodeSubAgileRoute, controller.AgentCreateUserDeployment)
heicodeSwarmRoute := apiRouter.Group("/heicode/swarm")
heicodeSwarmRoute.Use(middleware.UserOrV2DeviceAuth())
registerHeicodeTaskRoutes(heicodeSwarmRoute, controller.AgentCreateUserSwarm)
// Agent orchestration control plane (minimal integration endpoints)
agentRoute := apiRouter.Group("/agent")
agentRoute.Use(middleware.AdminAuth())
{
agnetRoute.GET("/deployments", controller.AgnetListDeployments)
agnetRoute.POST("/deployments", controller.AgnetCreateDeployment)
agnetRoute.GET("/deployments/:deployment_id", controller.AgnetGetDeployment)
agnetRoute.POST("/deployments/:deployment_id/stop", controller.AgnetStopDeployment)
agnetRoute.GET("/deployments/:deployment_id/logs", controller.AgnetListDeploymentLogs)
agnetRoute.GET("/deployments/:deployment_id/metrics", controller.AgnetGetDeploymentMetrics)
agnetRoute.GET("/deployments/:deployment_id/events", controller.AgnetListDeploymentEvents)
agnetRoute.GET("/deployments/:deployment_id/sk-snapshots", controller.AgnetListSKSnapshots)
agnetRoute.POST("/sk-snapshots/resolve", controller.AgnetResolveSKSnapshots)
agnetRoute.GET("/runtime/health", controller.AgnetRuntimeHealth)
agnetRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgnetProjectDashboardSnapshot)
agnetRoute.GET("/audit-logs", controller.AgnetListAuditLogs)
agentRoute.GET("/deployments", controller.AgentListDeployments)
agentRoute.POST("/deployments", controller.AgentCreateDeployment)
agentRoute.GET("/deployments/:deployment_id", controller.AgentGetDeployment)
agentRoute.POST("/deployments/:deployment_id/stop", controller.AgentStopDeployment)
agentRoute.GET("/deployments/:deployment_id/logs", controller.AgentListDeploymentLogs)
agentRoute.GET("/deployments/:deployment_id/metrics", controller.AgentGetDeploymentMetrics)
agentRoute.GET("/deployments/:deployment_id/events", controller.AgentListDeploymentEvents)
agentRoute.GET("/deployments/:deployment_id/sk-snapshots", controller.AgentListSKSnapshots)
agentRoute.POST("/sk-snapshots/resolve", controller.AgentResolveSKSnapshots)
agentRoute.GET("/runtime/health", controller.AgentRuntimeHealth)
agentRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgentProjectDashboardSnapshot)
agentRoute.GET("/audit-logs", controller.AgentListAuditLogs)
}
}
}
+2 -2
View File
@@ -13,14 +13,14 @@
<meta name="title" content="Heicode Manager" />
<meta
name="description"
content="Heicode Manager — multi-tenant control plane for Agnet deployments, events and audit."
content="Heicode Manager — multi-tenant control plane for Agent deployments, events and audit."
/>
<meta property="og:title" content="Heicode Manager" />
<meta property="og:image" content="/logo.png?v=h-glass-2" />
<meta property="og:type" content="website" />
<meta name="theme-color" content="#7B6BE3" />
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.8fa3e0a349.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/9238.45c9c35ccf.js"></script><script defer src="/static/js/index.e57d82e6db.js"></script><link href="/static/css/index.fd51d44fe8.css" rel="stylesheet"></head>
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.829c7e3fad.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/6374.bc21d8b214.js"></script><script defer src="/static/js/index.99cba94710.js"></script><link href="/static/css/index.cc291c3921.css" rel="stylesheet"></head>
<body>
<div id="root"></div>
+1 -1
View File
@@ -13,7 +13,7 @@
<meta name="title" content="Heicode Manager" />
<meta
name="description"
content="Heicode Manager — multi-tenant control plane for Agnet deployments, events and audit."
content="Heicode Manager — multi-tenant control plane for Agent deployments, events and audit."
/>
<meta property="og:title" content="Heicode Manager" />
<meta property="og:image" content="/logo.png?v=h-glass-2" />
+1 -1
View File
@@ -105,7 +105,7 @@ export function QueryState(props: QueryStateProps) {
{loadingFallback ?? (
// Sensible default — three skeleton rows mimicking a card
// list. Callers pass `loadingFallback` to match their own
// grid (e.g. AgnetDeploymentsPage uses 2-col, audit uses
// grid (e.g. AgentDeploymentsPage uses 2-col, audit uses
// a vertical timeline).
<div className='space-y-3'>
<Skeleton className='h-24 w-full rounded-2xl' />
@@ -1,23 +1,23 @@
import { api } from '@/lib/api'
export type AgnetSubMode = 'agile' | 'waterfall'
export type AgentSubMode = 'agile' | 'waterfall'
/** Sub-agent cloud/runtime binding (passed to Agnet on deploy). */
export type AgnetRuntimeExecution = {
/** Sub-agent cloud/runtime binding (passed to Agent on deploy). */
export type AgentRuntimeExecution = {
profile_id?: string
cloud_principal_refs?: string[]
network_policy_ref?: string
}
/** SK allow/deny policy attached to the agent in the deployment plan. */
export type AgnetSKAccessPolicy = {
export type AgentSKAccessPolicy = {
policy_ref?: string
deny_skill_ids?: string[]
inherit_deployment_defaults?: boolean
}
/** Git-backed SK source (`type: git`). */
export type AgnetRepoRef = {
export type AgentRepoRef = {
connection_id?: string
repo_url?: string
ref: string
@@ -25,20 +25,20 @@ export type AgnetRepoRef = {
}
/** Single SK source entry (git or upload). */
export type AgnetSKSource = {
export type AgentSKSource = {
type?: string
artifact_id?: string
mime?: string
repo_ref?: AgnetRepoRef
repo_ref?: AgentRepoRef
}
export type AgnetAgentPlan = {
export type AgentAgentPlan = {
role_template: string
goal: string
default_model_id?: string
sk_sources?: AgnetSKSource[]
runtime_execution?: AgnetRuntimeExecution
sk_access_policy?: AgnetSKAccessPolicy
sk_sources?: AgentSKSource[]
runtime_execution?: AgentRuntimeExecution
sk_access_policy?: AgentSKAccessPolicy
resource_grants?: Array<{
grant_id?: string
resource_id?: string
@@ -56,13 +56,13 @@ export type AgnetAgentPlan = {
}>
}
export type AgnetBudget = {
export type AgentBudget = {
max_tokens: number
max_cost_usd: number
max_duration_sec: number
}
export type AgnetUserContext = {
export type AgentUserContext = {
user_id: string
email?: string
role?: string
@@ -70,15 +70,15 @@ export type AgnetUserContext = {
subscription_tier?: string
}
export type AgnetBillingContext = {
export type AgentBillingContext = {
provider?: 'newapi'
newapi_user_ref?: string
newapi_group?: string
quota_ref?: string
}
export type AgnetAgentRuntime = {
platform?: 'agnet'
export type AgentAgentRuntime = {
platform?: 'agent'
agents?: Array<{
role: string
model_ref: string
@@ -86,57 +86,57 @@ export type AgnetAgentRuntime = {
}>
}
export type AgnetConstraints = {
/** Runtime model allow-list for Agnet deployments; not a NewAPI billing map. */
export type AgentConstraints = {
/** Runtime model allow-list for Agent deployments; not a NewAPI billing map. */
allowed_model_ids?: string[]
}
export type AgnetOrchestrationMetadata = {
export type AgentOrchestrationMetadata = {
/**
* Compatibility field for Agnet routing scope; UI treats this as user scope,
* Compatibility field for Agent routing scope; UI treats this as user scope,
* not billing tenant.
*/
tenant_id: string
/**
* Compatibility field for Agnet routing scope; UI treats this as resource
* Compatibility field for Agent routing scope; UI treats this as resource
* scope, not project control.
*/
project_id: string
correlation_id: string
}
export type AgnetOrchestrationPlan = {
export type AgentOrchestrationPlan = {
intent_id: string
template_hint: string
objective: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
risk_level: 'low' | 'medium' | 'high'
budget: AgnetBudget
user_context: AgnetUserContext
billing_context?: AgnetBillingContext
agent_runtime?: AgnetAgentRuntime
agents: AgnetAgentPlan[]
constraints: AgnetConstraints
metadata: AgnetOrchestrationMetadata
budget: AgentBudget
user_context: AgentUserContext
billing_context?: AgentBillingContext
agent_runtime?: AgentAgentRuntime
agents: AgentAgentPlan[]
constraints: AgentConstraints
metadata: AgentOrchestrationMetadata
}
export type AgnetCreateDeploymentBody = {
orchestration_plan: AgnetOrchestrationPlan
export type AgentCreateDeploymentBody = {
orchestration_plan: AgentOrchestrationPlan
}
export type AgnetCreateDeploymentResult = {
export type AgentCreateDeploymentResult = {
deployment_id: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
status: string
agent_instances?: Array<{
instance_id?: string
role?: string
phase?: string
}>
permission_manifest?: AgnetPermissionManifest
permission_manifest?: AgentPermissionManifest
}
export type AgnetPermissionManifest = {
export type AgentPermissionManifest = {
user_id?: string
binding_scope?: string
agent_role?: string
@@ -153,9 +153,9 @@ export type AgnetPermissionManifest = {
}>
}
export type AgnetDeployment = {
export type AgentDeployment = {
deployment_id: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
status: string
phase: string
runtime_state?: string
@@ -165,24 +165,24 @@ export type AgnetDeployment = {
failure_reason?: string
created_at: string
updated_at: string
permission_manifest?: AgnetPermissionManifest
permission_manifest?: AgentPermissionManifest
orchestration_plan: {
intent_id?: string
template_hint?: string
objective?: string
sub_mode?: AgnetSubMode
sub_mode?: AgentSubMode
risk_level?: string
budget?: AgnetBudget
agents?: AgnetAgentPlan[]
constraints?: AgnetConstraints
metadata?: AgnetOrchestrationMetadata
budget?: AgentBudget
agents?: AgentAgentPlan[]
constraints?: AgentConstraints
metadata?: AgentOrchestrationMetadata
}
}
export type AgnetRuntimeDiagnostics = {
export type AgentRuntimeDiagnostics = {
deployment_id: string
runtime_mode?: 'agnet' | 'swarm' | string
sub_mode?: AgnetSubMode
runtime_mode?: 'agent' | 'swarm' | string
sub_mode?: AgentSubMode
runtime_deployment_id?: string
runtime_swarm_id?: string
data_source?: string
@@ -198,7 +198,7 @@ export type AgnetRuntimeDiagnostics = {
checked_at?: string
}
export type AgnetApprovalRequest = {
export type AgentApprovalRequest = {
approval_id: string
user_id: number
deployment_id?: string
@@ -221,10 +221,10 @@ export type AgnetApprovalRequest = {
decided_at?: number
created_at?: number
updated_at?: number
credential_lease?: AgnetCredentialLease
credential_lease?: AgentCredentialLease
}
export type AgnetCredentialLease = {
export type AgentCredentialLease = {
lease_id: string
credential_ref: string
approval_id: string
@@ -273,10 +273,10 @@ export type GitSourcePayload = {
}
// Platform-recommended role catalog shape. Mirrors backend
// `AgnetRoleTemplate` in controller/agnet_role_template.go. The
// `AgentRoleTemplate` in controller/agent_role_template.go. The
// six canonical roles come from docs/product-package §13.3.3 —
// keys are stable identifiers, display strings can be translated.
export type AgnetRoleTemplate = {
export type AgentRoleTemplate = {
key: string
display_name: string
summary: string
@@ -288,12 +288,12 @@ export type AgnetRoleTemplate = {
// Cached at module level — the canonical six-role catalog doesn't
// change between page loads, so we avoid an extra request every
// time the create-deployment sheet opens.
let _roleTemplateCache: AgnetRoleTemplate[] | null = null
let _roleTemplateCache: AgentRoleTemplate[] | null = null
export async function listAgnetRoleTemplates(): Promise<AgnetRoleTemplate[]> {
export async function listAgentRoleTemplates(): Promise<AgentRoleTemplate[]> {
if (_roleTemplateCache) return _roleTemplateCache
const res = await api.get<ApiEnvelope<{ items?: AgnetRoleTemplate[] }>>(
'/api/agnet/role-templates'
const res = await api.get<ApiEnvelope<{ items?: AgentRoleTemplate[] }>>(
'/api/agent/role-templates'
)
const items = res.data?.data?.items ?? []
if (items.length > 0) {
@@ -302,16 +302,16 @@ export async function listAgnetRoleTemplates(): Promise<AgnetRoleTemplate[]> {
return items
}
export async function listAgnetDeployments(): Promise<AgnetDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
'/api/agnet/user/deployments'
export async function listAgentDeployments(): Promise<AgentDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentDeployment[] }>>(
'/api/agent/user/deployments'
)
return res.data?.data?.items ?? []
}
export async function listAgnetDeploymentsQuiet(): Promise<AgnetDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
'/api/agnet/user/deployments',
export async function listAgentDeploymentsQuiet(): Promise<AgentDeployment[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentDeployment[] }>>(
'/api/agent/user/deployments',
{
skipBusinessError: true,
skipErrorHandler: true,
@@ -321,11 +321,11 @@ export async function listAgnetDeploymentsQuiet(): Promise<AgnetDeployment[]> {
return res.data?.data?.items ?? []
}
export async function createAgnetDeployment(
body: AgnetCreateDeploymentBody
): Promise<AgnetCreateDeploymentResult> {
const res = await api.post<ApiEnvelope<AgnetCreateDeploymentResult>>(
'/api/agnet/user/deployments',
export async function createAgentDeployment(
body: AgentCreateDeploymentBody
): Promise<AgentCreateDeploymentResult> {
const res = await api.post<ApiEnvelope<AgentCreateDeploymentResult>>(
'/api/agent/user/deployments',
body
)
const env = res.data
@@ -339,28 +339,28 @@ export async function createAgnetDeployment(
return data
}
export async function getAgnetDeploymentEvents(deploymentId: string) {
export async function getAgentDeploymentEvents(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/user/deployments/${deploymentId}/events`)
>(`/api/agent/user/deployments/${deploymentId}/events`)
return res.data?.data?.items ?? []
}
export async function getAgnetDeploymentArtifacts(deploymentId: string) {
export async function getAgentDeploymentArtifacts(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/user/deployments/${deploymentId}/artifacts`)
>(`/api/agent/user/deployments/${deploymentId}/artifacts`)
return res.data?.data?.items ?? []
}
export async function getAgnetDeploymentSKSnapshots(deploymentId: string) {
export async function getAgentDeploymentSKSnapshots(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/user/deployments/${deploymentId}/sk-snapshots`)
>(`/api/agent/user/deployments/${deploymentId}/sk-snapshots`)
return res.data?.data?.items ?? []
}
export async function getAgnetDeploymentTimeline(deploymentId: string) {
export async function getAgentDeploymentTimeline(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{
callbacks?: Array<Record<string, unknown>>
@@ -368,7 +368,7 @@ export async function getAgnetDeploymentTimeline(deploymentId: string) {
sk_snapshots?: Array<Record<string, unknown>>
timeline?: Array<Record<string, unknown>>
}>
>(`/api/agnet/user/deployments/${deploymentId}/timeline`)
>(`/api/agent/user/deployments/${deploymentId}/timeline`)
return (
res.data?.data ?? {
callbacks: [],
@@ -379,11 +379,11 @@ export async function getAgnetDeploymentTimeline(deploymentId: string) {
)
}
export async function getAgnetRuntimeDiagnostics(
export async function getAgentRuntimeDiagnostics(
deploymentId: string
): Promise<AgnetRuntimeDiagnostics | null> {
const res = await api.get<ApiEnvelope<AgnetRuntimeDiagnostics>>(
`/api/agnet/user/deployments/${deploymentId}/runtime-diagnostics`,
): Promise<AgentRuntimeDiagnostics | null> {
const res = await api.get<ApiEnvelope<AgentRuntimeDiagnostics>>(
`/api/agent/user/deployments/${deploymentId}/runtime-diagnostics`,
{
skipBusinessError: true,
skipErrorHandler: true,
@@ -393,46 +393,46 @@ export async function getAgnetRuntimeDiagnostics(
return res.data?.data ?? null
}
export async function simulateAgnetDeploymentEvents(
export async function simulateAgentDeploymentEvents(
deploymentId: string,
events?: string[]
): Promise<{ deployment_id: string; simulated: boolean; total: number }> {
const res = await api.post<
ApiEnvelope<{ deployment_id: string; simulated: boolean; total: number }>
>(`/api/agnet/user/deployments/${deploymentId}/simulate-events`, {
>(`/api/agent/user/deployments/${deploymentId}/simulate-events`, {
events: events ?? [],
})
const env = res.data
if (!env?.success || !env.data) {
throw new Error(env?.message || 'simulateAgnetDeploymentEvents failed')
throw new Error(env?.message || 'simulateAgentDeploymentEvents failed')
}
return env.data
}
export async function getAgnetAuditLogs() {
export async function getAgentAuditLogs() {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>('/api/agnet/audit-logs')
>('/api/agent/audit-logs')
return res.data?.data?.items ?? []
}
export async function listAgnetApprovals(params?: {
export async function listAgentApprovals(params?: {
status?: string
deployment_id?: string
}): Promise<AgnetApprovalRequest[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetApprovalRequest[] }>>(
'/api/agnet/approvals',
}): Promise<AgentApprovalRequest[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentApprovalRequest[] }>>(
'/api/agent/approvals',
{ params }
)
return res.data?.data?.items ?? []
}
export async function approveAgnetApproval(
export async function approveAgentApproval(
approvalId: string,
reason?: string
): Promise<AgnetApprovalRequest> {
const res = await api.post<ApiEnvelope<AgnetApprovalRequest>>(
`/api/agnet/approvals/${approvalId}/approve`,
): Promise<AgentApprovalRequest> {
const res = await api.post<ApiEnvelope<AgentApprovalRequest>>(
`/api/agent/approvals/${approvalId}/approve`,
{ reason }
)
if (!res.data?.success || !res.data.data) {
@@ -441,12 +441,12 @@ export async function approveAgnetApproval(
return res.data.data
}
export async function rejectAgnetApproval(
export async function rejectAgentApproval(
approvalId: string,
reason?: string
): Promise<AgnetApprovalRequest> {
const res = await api.post<ApiEnvelope<AgnetApprovalRequest>>(
`/api/agnet/approvals/${approvalId}/reject`,
): Promise<AgentApprovalRequest> {
const res = await api.post<ApiEnvelope<AgentApprovalRequest>>(
`/api/agent/approvals/${approvalId}/reject`,
{ reason }
)
if (!res.data?.success || !res.data.data) {
@@ -455,23 +455,23 @@ export async function rejectAgnetApproval(
return res.data.data
}
export async function listAgnetCredentialLeases(params?: {
export async function listAgentCredentialLeases(params?: {
status?: string
deployment_id?: string
}): Promise<AgnetCredentialLease[]> {
const res = await api.get<ApiEnvelope<{ items?: AgnetCredentialLease[] }>>(
'/api/agnet/credential-leases',
}): Promise<AgentCredentialLease[]> {
const res = await api.get<ApiEnvelope<{ items?: AgentCredentialLease[] }>>(
'/api/agent/credential-leases',
{ params }
)
return res.data?.data?.items ?? []
}
export async function revokeAgnetCredentialLease(
export async function revokeAgentCredentialLease(
leaseId: string,
reason?: string
): Promise<AgnetCredentialLease> {
const res = await api.post<ApiEnvelope<AgnetCredentialLease>>(
`/api/agnet/credential-leases/${leaseId}/revoke`,
): Promise<AgentCredentialLease> {
const res = await api.post<ApiEnvelope<AgentCredentialLease>>(
`/api/agent/credential-leases/${leaseId}/revoke`,
{ reason }
)
if (!res.data?.success || !res.data.data) {
@@ -480,10 +480,10 @@ export async function revokeAgnetCredentialLease(
return res.data.data
}
export async function getAgnetSnapshots(deploymentId: string) {
export async function getAgentSnapshots(deploymentId: string) {
const res = await api.get<
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
>(`/api/agnet/deployments/${deploymentId}/sk-snapshots`, {
>(`/api/agent/deployments/${deploymentId}/sk-snapshots`, {
skipBusinessError: true,
skipErrorHandler: true,
} as Record<string, unknown>)
@@ -40,13 +40,13 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import {
createAgnetDeployment,
listAgnetRoleTemplates,
type AgnetAgentPlan,
type AgnetOrchestrationPlan,
type AgnetRoleTemplate,
type AgnetSKSource,
type AgnetSubMode,
createAgentDeployment,
listAgentRoleTemplates,
type AgentAgentPlan,
type AgentOrchestrationPlan,
type AgentRoleTemplate,
type AgentSKSource,
type AgentSubMode,
} from './api'
type ResourceType =
@@ -240,7 +240,7 @@ function formatRiskLevel(
return t('High')
}
function formatSubMode(value: AgnetSubMode, t: (key: string) => string) {
function formatSubMode(value: AgentSubMode, t: (key: string) => string) {
if (value === 'agile') return t('Agile')
return t('Waterfall')
}
@@ -261,11 +261,11 @@ function formatRoleLabel(value: string, t: (key: string) => string) {
function formatRoleDisplayName(value: string, t: (key: string) => string) {
const name = value.trim().toLowerCase()
const labels: Record<string, string> = {
'product agnet': t('Product Agnet'),
'frontend agnet': t('Frontend Agnet'),
'backend agnet': t('Backend Agnet'),
'reviewer agnet': t('Reviewer Agnet'),
'ops agnet': t('Ops Agnet'),
'product agent': t('Product Agent'),
'frontend agent': t('Frontend Agent'),
'backend agent': t('Backend Agent'),
'reviewer agent': t('Reviewer Agent'),
'ops agent': t('Ops Agent'),
}
return labels[name] || value
}
@@ -293,15 +293,15 @@ function buildAgent(
resourceScopeRef: string
correlationId: string
}
): AgnetAgentPlan {
let sk_sources: AgnetSKSource[] | undefined
): AgentAgentPlan {
let sk_sources: AgentSKSource[] | undefined
const raw = row.sk_sources_json.trim()
if (raw) {
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) {
throw new Error('sk_sources_must_be_array')
}
sk_sources = parsed as AgnetSKSource[]
sk_sources = parsed as AgentSKSource[]
}
const role = row.role_template.trim()
@@ -404,7 +404,7 @@ function SectionTitle({
)
}
export function CreateAgnetDeploymentSheet({
export function CreateAgentDeploymentSheet({
open,
onOpenChange,
}: {
@@ -419,7 +419,7 @@ export function CreateAgnetDeploymentSheet({
const [step, setStep] = useState('idea')
const [templateHint, setTemplateHint] = useState(defaultTemplate.id)
const [objective, setObjective] = useState(defaultTemplate.objective)
const [subMode, setSubMode] = useState<AgnetSubMode>('agile')
const [subMode, setSubMode] = useState<AgentSubMode>('agile')
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high'>(
defaultTemplate.risk
)
@@ -442,11 +442,11 @@ export function CreateAgnetDeploymentSheet({
// subsequent sheet opens don't re-fetch. Empty array fallback
// means the sheet stays usable if the catalog endpoint is down —
// the role picker falls back to free-text input.
const [roleTemplates, setRoleTemplates] = useState<AgnetRoleTemplate[]>([])
const [roleTemplates, setRoleTemplates] = useState<AgentRoleTemplate[]>([])
useEffect(() => {
if (!open) return
let cancelled = false
listAgnetRoleTemplates()
listAgentRoleTemplates()
.then((items) => {
if (!cancelled) setRoleTemplates(items)
})
@@ -556,7 +556,7 @@ export function CreateAgnetDeploymentSheet({
risk_level: riskLevel,
resource_scope_ref: resourceScopeRef.trim(),
agent_runtime: {
platform: 'agnet',
platform: 'agent',
agents: previewAgents
.filter((agent) => agent.role_template && agent.default_model_id)
.map((agent) => ({
@@ -624,7 +624,7 @@ export function CreateAgnetDeploymentSheet({
mutationFn: async () => {
const intentId = crypto.randomUUID()
const allowed = splitComma(allowedModels)
let agentPlans: AgnetAgentPlan[]
let agentPlans: AgentAgentPlan[]
try {
agentPlans = agents.map((row, index) =>
buildAgent(row, index, {
@@ -648,7 +648,7 @@ export function CreateAgnetDeploymentSheet({
instance_count: 1,
}))
const plan: AgnetOrchestrationPlan = {
const plan: AgentOrchestrationPlan = {
intent_id: intentId,
template_hint: templateHint.trim(),
objective: objective.trim(),
@@ -674,7 +674,7 @@ export function CreateAgnetDeploymentSheet({
agent_runtime:
runtimeAgents.length > 0
? {
platform: 'agnet',
platform: 'agent',
agents: runtimeAgents,
}
: undefined,
@@ -689,15 +689,15 @@ export function CreateAgnetDeploymentSheet({
},
}
return createAgnetDeployment({ orchestration_plan: plan })
return createAgentDeployment({ orchestration_plan: plan })
},
onSuccess: (data) => {
toast.success(
t('Agnet deployment created', {
t('Agent deployment created', {
deployment_id: data.deployment_id,
}) as string
)
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'deployments'] })
onOpenChange(false)
resetForm()
},
@@ -850,7 +850,7 @@ export function CreateAgnetDeploymentSheet({
<Label>{t('Sub mode')}</Label>
<Select
value={subMode}
onValueChange={(v) => setSubMode(v as AgnetSubMode)}
onValueChange={(v) => setSubMode(v as AgentSubMode)}
>
<SelectTrigger className='h-9'>
<SelectValue />
@@ -896,7 +896,7 @@ export function CreateAgnetDeploymentSheet({
/>
</div>
<div className='grid gap-2'>
<Label>{t('Agnet allowed models comma')}</Label>
<Label>{t('Agent allowed models comma')}</Label>
<Input
value={allowedModels}
onChange={(e) => setAllowedModels(e.target.value)}
@@ -952,7 +952,7 @@ export function CreateAgnetDeploymentSheet({
icon={Bot}
title={t('Role cards')}
hint={t(
'Each card maps one child Agnet role to a runtime model and one bounded resource grant.'
'Each card maps one child Agent role to a runtime model and one bounded resource grant.'
)}
/>
<Button
@@ -1002,7 +1002,7 @@ export function CreateAgnetDeploymentSheet({
<div className='grid gap-3'>
<div className='grid gap-2 sm:grid-cols-3'>
{/* Role picker — bound to the canonical six-role
catalog from /api/agnet/role-templates. Falls
catalog from /api/agent/role-templates. Falls
back to a free-text input if the catalog
failed to load. */}
{roleTemplates.length > 0 ? (
@@ -1039,7 +1039,7 @@ export function CreateAgnetDeploymentSheet({
/>
)}
<Input
placeholder={t('Agnet runtime model id')}
placeholder={t('Agent runtime model id')}
value={agent.default_model_id}
onChange={(e) =>
updateAgent(index, {
@@ -1336,7 +1336,7 @@ export function CreateAgnetDeploymentSheet({
0 ? (
<p className='text-muted-foreground mt-2 text-sm italic'>
{t(
'No resources bound yet — Agnet will run with no external data access.'
'No resources bound yet — Agent will run with no external data access.'
)}
</p>
) : (
@@ -61,25 +61,25 @@ import {
import { Skeleton } from '@/components/ui/skeleton'
import { QueryState } from '@/components/query-state'
import {
approveAgnetApproval,
getAgnetDeploymentEvents,
getAgnetRuntimeDiagnostics,
getAgnetDeploymentTimeline,
listAgnetApprovals,
listAgnetCredentialLeases,
listAgnetDeployments,
rejectAgnetApproval,
revokeAgnetCredentialLease,
simulateAgnetDeploymentEvents,
type AgnetApprovalRequest,
type AgnetCredentialLease,
type AgnetDeployment,
type AgnetRuntimeDiagnostics,
type AgnetRuntimeExecution,
type AgnetSKAccessPolicy,
approveAgentApproval,
getAgentDeploymentEvents,
getAgentRuntimeDiagnostics,
getAgentDeploymentTimeline,
listAgentApprovals,
listAgentCredentialLeases,
listAgentDeployments,
rejectAgentApproval,
revokeAgentCredentialLease,
simulateAgentDeploymentEvents,
type AgentApprovalRequest,
type AgentCredentialLease,
type AgentDeployment,
type AgentRuntimeDiagnostics,
type AgentRuntimeExecution,
type AgentSKAccessPolicy,
} from './api'
import { AzureCloudBindingSheet } from './azure-cloud-binding-sheet'
import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
import { CreateAgentDeploymentSheet } from './create-agent-deployment-sheet'
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
@@ -260,7 +260,7 @@ function MetaPill({
)
}
function describeRiskLevel(dep: AgnetDeployment): {
function describeRiskLevel(dep: AgentDeployment): {
label: string
tone: 'low' | 'mid' | 'high'
} {
@@ -288,7 +288,7 @@ function formatRiskLabel(label: string, t: (key: string) => string): string {
return label
}
function describeSubMode(dep: AgnetDeployment): string {
function describeSubMode(dep: AgentDeployment): string {
return dep.sub_mode || dep.orchestration_plan?.sub_mode || 'agile'
}
@@ -299,7 +299,7 @@ function formatSubModeLabel(mode: string, t: (key: string) => string): string {
return mode
}
function describeBudget(dep: AgnetDeployment): string {
function describeBudget(dep: AgentDeployment): string {
const budget = dep.orchestration_plan?.budget
if (!budget) {
const agents = dep.orchestration_plan?.agents?.length ?? 0
@@ -314,7 +314,7 @@ function describeBudget(dep: AgnetDeployment): string {
return parts.length > 0 ? parts.join(' / ') : '—'
}
function describeScope(dep: AgnetDeployment): string {
function describeScope(dep: AgentDeployment): string {
const firstGrant = dep.orchestration_plan?.agents?.flatMap(
(agent) => agent.resource_grants || []
)[0]
@@ -452,7 +452,7 @@ function buildTaskFlowSummaries(
}
function collectResourceGrants(
dep: AgnetDeployment
dep: AgentDeployment
): Record<string, unknown>[] {
const manifestGrants = dep.permission_manifest?.resource_grants
if (manifestGrants && manifestGrants.length > 0) {
@@ -468,7 +468,7 @@ function collectResourceGrants(
)
}
function describeSecretRefs(dep: AgnetDeployment): string {
function describeSecretRefs(dep: AgentDeployment): string {
const grants = collectResourceGrants(dep)
const refs = grants.filter((grant) => {
const secretRef = grant.secret_ref
@@ -568,17 +568,17 @@ function runtimeWarningLabel(
}
function runtimeModeLabel(
diagnostics: AgnetRuntimeDiagnostics | null | undefined,
diagnostics: AgentRuntimeDiagnostics | null | undefined,
t: (key: string) => string
): string {
const mode = String(diagnostics?.runtime_mode || '').toLowerCase()
if (mode === 'swarm') return t('Swarm mode')
if (mode === 'agnet') return t('Ordinary sub mode')
if (mode === 'agent') return t('Ordinary sub mode')
return mode || '—'
}
function runtimeAgentRows(
diagnostics: AgnetRuntimeDiagnostics | null | undefined
diagnostics: AgentRuntimeDiagnostics | null | undefined
) {
return (diagnostics?.agents ?? []).slice(0, 4).map((agent, idx) => ({
id: String(agent.agent_id || agent.instance_id || idx),
@@ -592,7 +592,7 @@ function RuntimeDiagnosticsPanel({
diagnostics,
isLoading,
}: {
diagnostics?: AgnetRuntimeDiagnostics | null
diagnostics?: AgentRuntimeDiagnostics | null
isLoading: boolean
}) {
const { t } = useTranslation()
@@ -685,7 +685,7 @@ function RuntimeDiagnosticsPanel({
)
}
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
function RunDetailPanel({ dep }: { dep: AgentDeployment }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const currentUser = useAuthStore((state) => state.auth.user)
@@ -694,21 +694,21 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
const risk = describeRiskLevel(dep)
const grants = collectResourceGrants(dep)
const runtimeDiagnosticsQuery = useQuery({
queryKey: ['agnet', 'runtime-diagnostics', dep.deployment_id],
queryFn: () => getAgnetRuntimeDiagnostics(dep.deployment_id),
queryKey: ['agent', 'runtime-diagnostics', dep.deployment_id],
queryFn: () => getAgentRuntimeDiagnostics(dep.deployment_id),
enabled: Boolean(dep.deployment_id),
refetchInterval: 30_000,
})
const runtimeDiagnostics = runtimeDiagnosticsQuery.data
const simulateMutation = useMutation({
mutationFn: () => simulateAgnetDeploymentEvents(dep.deployment_id),
mutationFn: () => simulateAgentDeploymentEvents(dep.deployment_id),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'deployments'] })
void queryClient.invalidateQueries({
queryKey: ['agnet', 'deployment-events', dep.deployment_id],
queryKey: ['agent', 'deployment-events', dep.deployment_id],
})
void queryClient.invalidateQueries({
queryKey: ['agnet', 'deployment-timeline', dep.deployment_id],
queryKey: ['agent', 'deployment-timeline', dep.deployment_id],
})
toast.success(t('Simulated events recorded'))
},
@@ -986,7 +986,7 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
// Deployments page
// =============================================================================
export function AgnetDeploymentsPage() {
export function AgentDeploymentsPage() {
const { t } = useTranslation()
const [filter, setFilter] = useState<'all' | StatusKey>('all')
const [keyword, setKeyword] = useState('')
@@ -999,8 +999,8 @@ export function AgnetDeploymentsPage() {
error: deploymentsError,
refetch: refetchDeployments,
} = useQuery({
queryKey: ['agnet', 'deployments'],
queryFn: listAgnetDeployments,
queryKey: ['agent', 'deployments'],
queryFn: listAgentDeployments,
refetchInterval: 30_000,
retry: false, // QueryState handles error display; no silent retries
})
@@ -1034,7 +1034,7 @@ export function AgnetDeploymentsPage() {
<PageSurface
title={t('Task overview')}
subtitle={t(
'Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.'
'Status, latest activity and last update for every Agent task you launched. Details live in the desktop client.'
)}
toolbar={
<>
@@ -1143,7 +1143,7 @@ export function AgnetDeploymentsPage() {
</div>
</QueryState>
</PageSurface>
<CreateAgnetDeploymentSheet
<CreateAgentDeploymentSheet
open={createOpen}
onOpenChange={setCreateOpen}
/>
@@ -1199,8 +1199,8 @@ export function AgnetDeploymentsPage() {
function RunAuditTimeline({ deploymentId }: { deploymentId: string }) {
const { t } = useTranslation()
const { data = [], isLoading } = useQuery({
queryKey: ['agnet', 'deployment-events', deploymentId],
queryFn: () => getAgnetDeploymentEvents(deploymentId),
queryKey: ['agent', 'deployment-events', deploymentId],
queryFn: () => getAgentDeploymentEvents(deploymentId),
enabled: Boolean(deploymentId),
refetchInterval: 15_000,
})
@@ -1286,8 +1286,8 @@ function RunAuditTimeline({ deploymentId }: { deploymentId: string }) {
function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
const { t } = useTranslation()
const { data, isLoading } = useQuery({
queryKey: ['agnet', 'deployment-timeline', deploymentId],
queryFn: () => getAgnetDeploymentTimeline(deploymentId),
queryKey: ['agent', 'deployment-timeline', deploymentId],
queryFn: () => getAgentDeploymentTimeline(deploymentId),
enabled: Boolean(deploymentId),
refetchInterval: 15_000,
})
@@ -1490,7 +1490,7 @@ function RunRelatedRecordsPanel({ deploymentId }: { deploymentId: string }) {
)}
{Boolean(item.artifact_id) && (
<a
href={`/api/agnet/user/deployments/${encodeURIComponent(deploymentId)}/artifacts/${encodeURIComponent(String(item.artifact_id))}/content`}
href={`/api/agent/user/deployments/${encodeURIComponent(deploymentId)}/artifacts/${encodeURIComponent(String(item.artifact_id))}/content`}
target='_blank'
rel='noreferrer'
className='text-primary mt-2 inline-flex items-center gap-1 text-[11px] font-medium hover:underline'
@@ -1606,13 +1606,13 @@ function classifyEventLevel(entry: Record<string, unknown>): EventLevel {
return 'info'
}
export function AgnetEventsPage() {
export function AgentEventsPage() {
const { t } = useTranslation()
const [level, setLevel] = useState<EventLevel>('all')
const deploymentsQuery = useQuery({
queryKey: ['agnet', 'deployments'],
queryFn: listAgnetDeployments,
queryKey: ['agent', 'deployments'],
queryFn: listAgentDeployments,
})
const deployments = deploymentsQuery.data ?? []
const [activeDeployment, setActiveDeployment] = useState<string | undefined>(
@@ -1621,8 +1621,8 @@ export function AgnetEventsPage() {
const effectiveDeployment = activeDeployment ?? deployments[0]?.deployment_id
const eventsQuery = useQuery({
queryKey: ['agnet', 'events', effectiveDeployment],
queryFn: () => getAgnetDeploymentEvents(effectiveDeployment as string),
queryKey: ['agent', 'events', effectiveDeployment],
queryFn: () => getAgentDeploymentEvents(effectiveDeployment as string),
enabled: Boolean(effectiveDeployment),
})
@@ -1636,7 +1636,7 @@ export function AgnetEventsPage() {
<PageSurface
title={t('Events')}
subtitle={t(
'Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.'
'Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.'
)}
toolbar={
<>
@@ -1919,14 +1919,14 @@ function formatUnixMs(value?: number) {
return new Date(value).toLocaleString()
}
function AgnetApprovalCard({
function AgentApprovalCard({
approval,
approveBusy,
rejectBusy,
onApprove,
onReject,
}: {
approval: AgnetApprovalRequest
approval: AgentApprovalRequest
approveBusy: boolean
rejectBusy: boolean
onApprove: () => void
@@ -1977,12 +1977,12 @@ function AgnetApprovalCard({
)
}
function AgnetLeaseCard({
function AgentLeaseCard({
lease,
busy,
onRevoke,
}: {
lease: AgnetCredentialLease
lease: AgentCredentialLease
busy: boolean
onRevoke: () => void
}) {
@@ -2013,7 +2013,7 @@ function AgnetLeaseCard({
)
}
export function AgnetAuditPage() {
export function AgentAuditPage() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [scope, setScope] = useState('')
@@ -2021,30 +2021,30 @@ export function AgnetAuditPage() {
const [actionFilter, setActionFilter] = useState('')
const approvalsQuery = useQuery({
queryKey: ['agnet', 'approvals', 'pending'],
queryFn: () => listAgnetApprovals({ status: 'pending' }),
queryKey: ['agent', 'approvals', 'pending'],
queryFn: () => listAgentApprovals({ status: 'pending' }),
refetchInterval: 30_000,
})
const leasesQuery = useQuery({
queryKey: ['agnet', 'credential-leases', 'active'],
queryFn: () => listAgnetCredentialLeases({ status: 'active' }),
queryKey: ['agent', 'credential-leases', 'active'],
queryFn: () => listAgentCredentialLeases({ status: 'active' }),
refetchInterval: 30_000,
})
const refreshApprovalState = () => {
void queryClient.invalidateQueries({ queryKey: ['agnet', 'approvals'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'approvals'] })
void queryClient.invalidateQueries({
queryKey: ['agnet', 'credential-leases'],
queryKey: ['agent', 'credential-leases'],
})
void queryClient.invalidateQueries({
queryKey: ['heicode', 'agnet', 'audit'],
queryKey: ['heicode', 'agent', 'audit'],
})
}
const approveMutation = useMutation({
mutationFn: (approvalId: string) =>
approveAgnetApproval(approvalId, t('Approved from Manager audit page')),
approveAgentApproval(approvalId, t('Approved from Manager audit page')),
onSuccess: () => {
toast.success(t('Approval accepted'))
refreshApprovalState()
@@ -2053,7 +2053,7 @@ export function AgnetAuditPage() {
const rejectMutation = useMutation({
mutationFn: (approvalId: string) =>
rejectAgnetApproval(approvalId, t('Rejected from Manager audit page')),
rejectAgentApproval(approvalId, t('Rejected from Manager audit page')),
onSuccess: () => {
toast.success(t('Approval rejected'))
refreshApprovalState()
@@ -2062,7 +2062,7 @@ export function AgnetAuditPage() {
const revokeMutation = useMutation({
mutationFn: (leaseId: string) =>
revokeAgnetCredentialLease(leaseId, t('Revoked from Manager audit page')),
revokeAgentCredentialLease(leaseId, t('Revoked from Manager audit page')),
onSuccess: () => {
toast.success(t('Credential lease revoked'))
refreshApprovalState()
@@ -2075,7 +2075,7 @@ export function AgnetAuditPage() {
error: auditError,
refetch: refetchAudit,
} = useQuery({
queryKey: ['heicode', 'agnet', 'audit'],
queryKey: ['heicode', 'agent', 'audit'],
queryFn: () => listMcpAuditLogs({ limit: 200 }),
refetchInterval: 60_000,
retry: false,
@@ -2139,7 +2139,7 @@ export function AgnetAuditPage() {
</h3>
<p className='text-muted-foreground text-xs'>
{t(
'Approve or reject high-risk Agnet operations before credentials are leased.'
'Approve or reject high-risk Agent operations before credentials are leased.'
)}
</p>
</div>
@@ -2156,7 +2156,7 @@ export function AgnetAuditPage() {
>
<div className='grid gap-3'>
{(approvalsQuery.data ?? []).map((approval) => (
<AgnetApprovalCard
<AgentApprovalCard
key={approval.approval_id}
approval={approval}
approveBusy={approveMutation.isPending}
@@ -2193,7 +2193,7 @@ export function AgnetAuditPage() {
>
<div className='grid gap-3'>
{(leasesQuery.data ?? []).map((lease) => (
<AgnetLeaseCard
<AgentLeaseCard
key={lease.lease_id}
lease={lease}
busy={revokeMutation.isPending}
@@ -2232,12 +2232,12 @@ export function AgnetAuditPage() {
// resource-binding slice for Work/Runs.
// =============================================================================
// AgnetSKSourcesPage — “准备清单” wizard. Frames the page as a 4-step list
// AgentSKSourcesPage — “准备清单” wizard. Frames the page as a 4-step list
// (代码 / 文档 / 云账号 / 推荐摘要) per docs/product-package/10 §"Manager 准备清单"
// + /11 §5. Does not expose repo_url / ref / paths / usage / tenant_id as the
// main flow — those move into a “手动补充”次级 sheet only opened when the user
// clicks “连接代码仓库 → 高级补充”.
// AgnetSKSourcesPage — 准备清单 wizard.
// AgentSKSourcesPage — 准备清单 wizard.
//
// Data layer switched (commit ?) from the Heicode-local git_sources controller
// to mcp-server §2 ResourceBinding (/api/resources) per the contract docs
@@ -2258,7 +2258,7 @@ export function AgnetAuditPage() {
// (none) → secret_ref (Azure Key Vault azkv://...
// reference when the resource has
// credential material)
export function AgnetSKSourcesPage() {
export function AgentSKSourcesPage() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [advancedOpen, setAdvancedOpen] = useState(false)
@@ -2406,7 +2406,7 @@ export function AgnetSKSourcesPage() {
docSources.length > 0
? t('{{n}} doc source connected', { n: docSources.length })
: t(
'Link product requirements, design docs or wiki repos so Agnet has project context.'
'Link product requirements, design docs or wiki repos so Agent has project context.'
),
done: docSources.length > 0,
optional: true,
@@ -2457,7 +2457,7 @@ export function AgnetSKSourcesPage() {
<PageSurface
title={t('Preparation checklist')}
subtitle={t(
'Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agnet.'
'Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agent.'
)}
toolbar={
<span className='text-primary inline-flex items-center gap-1.5 rounded-full border border-[color-mix(in_oklch,var(--primary)_30%,var(--border))] bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-3 py-1 text-[11px] font-semibold tracking-[0.12em] uppercase'>
@@ -2524,7 +2524,7 @@ export function AgnetSKSourcesPage() {
disabled={!prereqsDone}
onClick={() => setSummaryOpen(true)}
>
{t('Confirm and launch Agnet')}
{t('Confirm and launch Agent')}
</Button>
) : step.key === 'cloud' ? (
// Open the Azure-specific sheet. Manager stores the secret in
@@ -2558,7 +2558,7 @@ export function AgnetSKSourcesPage() {
<p className='text-foreground font-medium'>{t('How this works')}</p>
<p className='mt-2'>
{t(
'Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.'
'Long-lived credentials are stored in the secret vault. Agent only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.'
)}
</p>
</div>
@@ -2812,8 +2812,8 @@ export function AgnetSKSourcesPage() {
// =============================================================================
// 推荐确认卡 — docs/product-package/10 §"推荐确认卡":
// 本次会做 / 本次允许使用 / 本次不会做 / 高危规则 / 预计消耗 / 启动 Agnet
// 「启动 Agnet」旁边写「参数由 Heicode 自动生成」。
// 本次会做 / 本次允许使用 / 本次不会做 / 高危规则 / 预计消耗 / 启动 Agent
// 「启动 Agent」旁边写「参数由 Heicode 自动生成」。
// 没有 JSON 编辑器、permission manifest、resource grant 表(§10 高级展开禁令)。
// =============================================================================
@@ -2859,12 +2859,12 @@ function RecommendationSummaryDialog({
const handleLaunch = () => {
setLaunching(true)
// Real /api/agnet/deployments POST is wired separately when the task
// Real /api/agent/deployments POST is wired separately when the task
// object backend lands. For now the summary card matches the docs spec
// visually; clicking captures intent + hands off to the desktop client.
setTimeout(() => {
toast.success(
t('Agnet launch staged. Continue the task in the desktop client.')
t('Agent launch staged. Continue the task in the desktop client.')
)
setLaunching(false)
onClose()
@@ -2880,7 +2880,7 @@ function RecommendationSummaryDialog({
{t('Recommendation summary')}
</p>
<h3 className='mt-1 text-lg font-semibold'>
{t('Confirm scope, risk and budget before launching Agnet')}
{t('Confirm scope, risk and budget before launching Agent')}
</h3>
<p className='text-muted-foreground mt-1 text-xs'>
{t(
@@ -2939,7 +2939,7 @@ function RecommendationSummaryDialog({
}}
>
<Rocket className='h-3.5 w-3.5' />
{launching ? t('Launching…') : t('Launch Agnet')}
{launching ? t('Launching…') : t('Launch Agent')}
</Button>
</div>
</footer>
@@ -2998,7 +2998,7 @@ function RecBlock({
// Templates / Agents (kept for backward compatibility — invoked by side routes)
// =============================================================================
export function AgnetTemplatesPage() {
export function AgentTemplatesPage() {
const { t } = useTranslation()
const templates = [
{
@@ -3035,7 +3035,7 @@ export function AgnetTemplatesPage() {
)
}
function runtimeSummary(rt: AgnetRuntimeExecution | undefined): boolean {
function runtimeSummary(rt: AgentRuntimeExecution | undefined): boolean {
if (!rt) return false
return Boolean(
(rt.profile_id && rt.profile_id.trim() !== '') ||
@@ -3044,7 +3044,7 @@ function runtimeSummary(rt: AgnetRuntimeExecution | undefined): boolean {
)
}
function policySummary(p: AgnetSKAccessPolicy | undefined): boolean {
function policySummary(p: AgentSKAccessPolicy | undefined): boolean {
if (!p) return false
return Boolean(
(p.policy_ref && p.policy_ref.trim() !== '') ||
@@ -3053,15 +3053,15 @@ function policySummary(p: AgnetSKAccessPolicy | undefined): boolean {
)
}
export function AgnetAgentsPage() {
export function AgentAgentsPage() {
const { t } = useTranslation()
const { data = [] } = useQuery({
queryKey: ['agnet', 'deployments'],
queryFn: listAgnetDeployments,
queryKey: ['agent', 'deployments'],
queryFn: listAgentDeployments,
})
const rows = useMemo(
() =>
data.flatMap((dep: AgnetDeployment) =>
data.flatMap((dep: AgentDeployment) =>
(dep.orchestration_plan?.agents || []).map((agent, idx) => ({
dep: dep.deployment_id,
id: `${dep.deployment_id}-${idx}`,
@@ -3078,7 +3078,7 @@ export function AgnetAgentsPage() {
return (
<PageSurface
title={t('Agents')}
subtitle={t('Agent declarations parsed from each Agnet deployment plan.')}
subtitle={t('Agent declarations parsed from each Agent deployment plan.')}
>
{rows.length === 0 ? (
<EmptySurface
@@ -3093,7 +3093,7 @@ export function AgnetAgentsPage() {
>
<p className='text-sm font-medium'>{row.role}</p>
<p className='text-muted-foreground mt-1 font-mono text-[11px] tracking-[0.12em] uppercase'>
{row.dep} · {t('Agnet runtime model')}: {row.runtimeModel}
{row.dep} · {t('Agent runtime model')}: {row.runtimeModel}
</p>
<p className='mt-2 text-sm'>{row.goal}</p>
{runtimeSummary(row.runtime) && (
@@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router'
type AgnetHubProps = {
type AgentHubProps = {
title: string
description: string
}
@@ -11,7 +11,7 @@ const quickLinks = [
{ title: 'Audit', to: '/audit' as const },
]
export function AgnetHub(props: AgnetHubProps) {
export function AgentHub(props: AgentHubProps) {
return (
<div className='mx-auto w-full max-w-5xl p-6'>
<div className='mb-5'>
+8 -8
View File
@@ -51,10 +51,10 @@ export function clearHeicodeTokens() {
}
/**
* 外部 Agnet 登录成功后,用 token 向本站校验身份并写入 Manager 会话 Cookie。
* 外部 Agent 登录成功后,用 token 向本站校验身份并写入 Manager 会话 Cookie。
* 不在本站再做密码校验;本地用户按需 JIT 创建。
*/
async function establishManagerSessionFromAgnet(): Promise<{
async function establishManagerSessionFromAgent(): Promise<{
managerUserId?: number
}> {
const access_token = readToken(ACCESS_TOKEN_KEY)
@@ -63,7 +63,7 @@ async function establishManagerSessionFromAgnet(): Promise<{
throw new Error('Missing Heicode access token')
}
const res = await api.post(
'/api/user/session/from-agnet',
'/api/user/session/from-agent',
{
access_token,
refresh_token: refresh_token || undefined,
@@ -101,13 +101,13 @@ async function establishManagerSessionFromAgnet(): Promise<{
return { managerUserId: body.data?.id }
}
export async function loginWithAgnetTokens(
export async function loginWithAgentTokens(
accessToken: string,
refreshToken?: string
): Promise<{ managerUserId?: number }> {
writeTokens(accessToken, refreshToken)
try {
return await establishManagerSessionFromAgnet()
return await establishManagerSessionFromAgent()
} catch (err) {
clearHeicodeTokens()
throw err
@@ -185,7 +185,7 @@ export async function login(payload: LoginPayload) {
if (res?.success) {
writeTokens(res.data?.token, res.data?.refreshToken)
try {
const sessionRes = await establishManagerSessionFromAgnet()
const sessionRes = await establishManagerSessionFromAgent()
managerUserId = sessionRes.managerUserId
} catch (syncErr) {
if (isTwoFactorRequiredError(syncErr)) {
@@ -330,7 +330,7 @@ export async function wechatLoginByCode(code: string): Promise<ApiResponse> {
// Registration
// ----------------------------------------------------------------------------
// User registration via Agnet (太极 AI PAD)
// User registration via Agent (太极 AI PAD)
export async function register(payload: RegisterPayload): Promise<ApiResponse & {
data?: {
token?: string
@@ -364,7 +364,7 @@ export async function register(payload: RegisterPayload): Promise<ApiResponse &
}
}
// Send email verification code via Agnet (太极 AI PAD)
// Send email verification code via Agent (太极 AI PAD)
export async function sendEmailVerification(
email: string,
_turnstile?: string
+1 -1
View File
@@ -58,7 +58,7 @@ export function AuthLayout({ children }: AuthLayoutProps) {
const pillars = [
{
icon: Workflow,
title: t('Agnet orchestration'),
title: t('Agent orchestration'),
desc: t('Plan, dispatch and monitor multi-agent runs across tenants.'),
},
{
@@ -77,7 +77,7 @@ export function useAuthRedirect() {
}
// 优先从本地 Manager 拉真实用户(含 role / status / quota),
// 因为 Agnet 上的 role 不一定与本地 JIT/管理员白名单同步后的角色一致。
// 因为 Agent 上的 role 不一定与本地 JIT/管理员白名单同步后的角色一致。
let userSet = false
try {
const selfRes = (await getSelf()) as {
@@ -100,7 +100,7 @@ export function useAuthRedirect() {
userSet = true
}
} catch {
// Fall through to Agnet /me / fallback below.
// Fall through to Agent /me / fallback below.
}
if (!userSet) {
@@ -28,7 +28,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile'
import { register, loginWithAgnetTokens, wechatLoginByCode } from '@/features/auth/api'
import { register, loginWithAgentTokens, wechatLoginByCode } from '@/features/auth/api'
import { LegalConsent } from '@/features/auth/components/legal-consent'
import { OAuthProviders } from '@/features/auth/components/oauth-providers'
import { registerFormSchema } from '@/features/auth/constants'
@@ -144,7 +144,7 @@ export function SignUpForm({
if (res?.success) {
toast.success(t('Account created!'))
if (res.data?.token) {
const sessionRes = await loginWithAgnetTokens(
const sessionRes = await loginWithAgentTokens(
res.data.token,
res.data.refreshToken
)
@@ -19,10 +19,10 @@ import {
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
getAgnetAuditLogs,
listAgnetDeployments,
type AgnetDeployment,
} from '@/features/agnet-console/api'
getAgentAuditLogs,
listAgentDeployments,
type AgentDeployment,
} from '@/features/agent-console/api'
type StatusKey = 'running' | 'success' | 'failed' | 'pending'
@@ -145,18 +145,18 @@ export function CockpitView() {
const deploymentsQuery = useQuery({
queryKey: ['cockpit', 'deployments'],
queryFn: listAgnetDeployments,
queryFn: listAgentDeployments,
refetchInterval: 30_000,
})
const auditQuery = useQuery({
queryKey: ['cockpit', 'audit'],
queryFn: getAgnetAuditLogs,
queryFn: getAgentAuditLogs,
refetchInterval: 60_000,
})
const stats = useMemo(() => {
const list: AgnetDeployment[] = deploymentsQuery.data ?? []
const list: AgentDeployment[] = deploymentsQuery.data ?? []
const counters: Record<StatusKey, number> = {
running: 0,
success: 0,
@@ -189,7 +189,7 @@ function IdeaInput({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
</h2>
<p className='text-muted-foreground mt-2 text-sm'>
{t(
'Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.'
'Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agent team.'
)}
</p>
</div>
@@ -293,7 +293,7 @@ function ContinueTasks({
) : recent.length === 0 ? (
<p className='bg-background/40 text-muted-foreground rounded-xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-4 text-center text-xs'>
{t(
'No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.'
'No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agent.'
)}
</p>
) : (
@@ -377,7 +377,7 @@ function TodayFocus({
tone: 'running',
title: t('Running'),
count: buckets.running.length,
hint: t('Active Agnet sub-loops'),
hint: t('Active Agent sub-loops'),
},
]
@@ -477,7 +477,7 @@ function HelperEntries({ t }: { t: ReturnType<typeof useTranslation>['t'] }) {
{
Icon: Rocket,
title: t('Task overview'),
desc: t('Status of every Agnet task you launched'),
desc: t('Status of every Agent task you launched'),
to: '/deployments',
},
]
@@ -15,7 +15,7 @@ const MODELS: ModelConfig[] = [
id: 'gpt-4o',
name: 'gpt-4o',
response:
'Agnet planners propose orchestration runs; the platform arbitrates risk and budget before any agent executes a step.',
'Agent planners propose orchestration runs; the platform arbitrates risk and budget before any agent executes a step.',
tokens: 27,
latency: 142,
badgeClass:
+6 -6
View File
@@ -45,9 +45,9 @@ import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import {
createAgnetDeployment,
type AgnetOrchestrationPlan,
} from '@/features/agnet-console/api'
createAgentDeployment,
type AgentOrchestrationPlan,
} from '@/features/agent-console/api'
const route = getRouteApi('/_authenticated/tasks/$id')
@@ -214,13 +214,13 @@ export function TaskCardView() {
role_templates: ['backend'],
default_model_id: 'claude-sonnet-4-6',
})
return createAgnetDeployment({
return createAgentDeployment({
orchestration_plan:
draft.orchestration_plan as unknown as AgnetOrchestrationPlan,
draft.orchestration_plan as unknown as AgentOrchestrationPlan,
})
},
onSuccess: (deployment) => {
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
void queryClient.invalidateQueries({ queryKey: ['agent', 'deployments'] })
toast.success(
t('Manager deployment created', {
deployment_id: deployment.deployment_id,
+1 -1
View File
@@ -91,7 +91,7 @@ export function Wallet(props: WalletProps) {
// the canonical balance source is mcp-server /api/user/heicode/balance
// (server-side wraps NewAPI admin token, returns the freshest figures).
// Fall back to NewAPI /api/user/self when the user hasn't yet been
// mirrored into NewAPI via from-agnet (HEICODE_USER_NOT_FOUND) so a
// mirrored into NewAPI via from-agent (HEICODE_USER_NOT_FOUND) so a
// brand-new account still sees something instead of empty stats.
const fetchUser = useCallback(async () => {
try {
+3 -3
View File
@@ -10,9 +10,9 @@ export type TopNavLink = {
external?: boolean
}
// Default top-nav modules — agnet command axis only.
// Default top-nav modules — agent command axis only.
// Pricing/Models/Channels are intentionally absent: Heicode Manager
// is a tenant + agnet control plane, not an API gateway storefront.
// is a tenant + agent control plane, not an API gateway storefront.
const DEFAULT_HEADER_NAV_MODULES = {
home: true,
overview: true,
@@ -26,7 +26,7 @@ const DEFAULT_HEADER_NAV_MODULES = {
* Backend format example (stringified JSON):
* {
* home: true,
* agnet: true,
* agent: true,
* deployments: true,
* events: true,
* audit: true,
@@ -2,7 +2,7 @@
"Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.": "Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.",
"Adjust filters or trigger a new orchestration plan.": "Adjust filters or trigger a new orchestration plan.",
"Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.": "Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Click to view image": "Click to view image",
"Client login API contract and integration screenshots": "Client login API contract and integration screenshots",
"Create a new code delivery run with checks.": "Create a new code delivery run with checks.",
@@ -13,7 +13,7 @@
"Git sources subtitle": "Bind Git repositories for SK, then review immutable snapshot anchors tied to each deployment.",
"Git-backed SK sources description": "Immutable snapshots from bound Git refs and uploads—wired into each run for audit.",
"Immutable, hash-verified context bundles wired to every run.": "Immutable, hash-verified context bundles wired to every run.",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"No audit entries match the filter.": "No audit entries match the filter.",
"No resolved snapshots hint": "Resolve snapshots from the control plane after Git-backed sk_sources are configured.",
"No deployments match the current filter": "No deployments match the current filter",
@@ -24,7 +24,7 @@
"Reset filters to see all entries.": "Reset filters to see all entries.",
"Resolve snapshots from the control plane to capture SK lineage.": "Resolve snapshots from the control plane to capture SK lineage.",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.": "Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.",
@@ -13,7 +13,7 @@
"Agentic development control plane": "Agentic development control plane",
"Agents": "Agents",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AIGC2D": "AIGC2D",
"All statuses": "All statuses",
"All usage logs": "All usage logs",
@@ -26,7 +26,7 @@
"Awaiting platform arbitration": "Awaiting platform arbitration",
"AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *",
"budget": "budget",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"channel": "channel",
"checkout.session.completed": "checkout.session.completed",
"checkout.session.expired": "checkout.session.expired",
@@ -96,7 +96,7 @@
"Insufficient permission": "Insufficient permission",
"Jimeng": "Jimeng",
"JustSong": "JustSong",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"LingYiWanWu": "LingYiWanWu",
"LinuxDO": "LinuxDO",
"Live code delivery runs": "Live code delivery runs",
@@ -161,7 +161,7 @@
"Running deployments": "Running deployments",
"Select deployment": "Select deployment",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"SiliconFlow": "SiliconFlow",
@@ -16,7 +16,7 @@
"Agentic development control plane": "Agentic development control plane",
"Agents": "Agents",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AI Proxy": "AI Proxy",
"AIGC2D": "AIGC2D",
"All statuses": "All statuses",
@@ -30,7 +30,7 @@
"AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *",
"Baidu V2": "Baidu V2",
"budget": "budget",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"channel": "channel",
"checkout.session.completed": "checkout.session.completed",
"checkout.session.expired": "checkout.session.expired",
@@ -100,7 +100,7 @@
"Insufficient permission": "Insufficient permission",
"Jimeng": "Jimeng",
"JustSong": "JustSong",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"LingYiWanWu": "LingYiWanWu",
"LinuxDO": "LinuxDO",
"Live code delivery runs": "Live code delivery runs",
@@ -164,7 +164,7 @@
"Running deployments": "Running deployments",
"Select deployment": "Select deployment",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"SiliconFlow": "SiliconFlow",
@@ -2,7 +2,7 @@
"Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.": "Account provisioning is handled by your platform administrator. Reach out to the Heicode operator to be added to a tenant.",
"Adjust filters or trigger a new orchestration plan.": "Adjust filters or trigger a new orchestration plan.",
"Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.": "Authenticate against the Heicode identity service. Tenant scope, role and SK access will be loaded automatically.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Click to view image": "Click to view image",
"Client login API contract and integration screenshots": "Client login API contract and integration screenshots",
"Create a new code delivery run with checks.": "Create a new code delivery run with checks.",
@@ -13,7 +13,7 @@
"Git sources subtitle": "Bind Git repositories for SK, then review immutable snapshot anchors tied to each deployment.",
"Git-backed SK sources description": "Immutable snapshots from bound Git refs and uploads—wired into each run for audit.",
"Immutable, hash-verified context bundles wired to every run.": "Immutable, hash-verified context bundles wired to every run.",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"No audit entries match the filter.": "No audit entries match the filter.",
"No resolved snapshots hint": "Resolve snapshots from the control plane after Git-backed sk_sources are configured.",
"No deployments match the current filter": "No deployments match the current filter",
@@ -24,7 +24,7 @@
"Reset filters to see all entries.": "Reset filters to see all entries.",
"Resolve snapshots from the control plane to capture SK lineage.": "Resolve snapshots from the control plane to capture SK lineage.",
"Set custom About HTML or URL in System Settings > General > About.": "Set custom About HTML or URL in System Settings > General > About.",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.": "Source → snapshot → hash chain. Every snapshot is immutable and tied to the orchestration plan.",
+30 -30
View File
@@ -78,7 +78,7 @@
"Actions": "Actions",
"active": "active",
"Active": "Active",
"Active Agnet sub-loops": "Active Agnet sub-loops",
"Active Agent sub-loops": "Active Agent sub-loops",
"Active Cache Count": "Active Cache Count",
"Active code delivery runs across tenants": "Active code delivery runs across tenants",
"Active Files": "Active Files",
@@ -176,7 +176,7 @@
"After invalidating, this subscription will be immediately deactivated. Historical records are not affected. Continue?": "After invalidating, this subscription will be immediately deactivated. Historical records are not affected. Continue?",
"After scanning, the binding will complete automatically": "After scanning, the binding will complete automatically",
"Agent": "Agent",
"Agent declarations parsed from each Agnet deployment plan.": "Agent declarations parsed from each Agnet deployment plan.",
"Agent declarations parsed from each Agent deployment plan.": "Agent declarations parsed from each Agent deployment plan.",
"Agent declarations parsed from each deployment plan.": "Agent declarations parsed from each deployment plan.",
"Agent ID *": "Agent ID *",
"Agentic development control plane": "Agentic development control plane",
@@ -184,12 +184,12 @@
"Aggregated usage metrics and trend charts.": "Aggregated usage metrics and trend charts.",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.",
"Agile Minimal": "Agile Minimal",
"Agnet allowed models comma": "Agnet runtime model IDs (comma-separated)",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agnet launch staged. Continue the task in the desktop client.": "Agnet launch staged. Continue the task in the desktop client.",
"Agnet orchestration": "Agnet orchestration",
"Agnet runtime model": "Agnet runtime model",
"Agnet runtime model id": "Agnet runtime model ID",
"Agent allowed models comma": "Agent runtime model IDs (comma-separated)",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Agent launch staged. Continue the task in the desktop client.": "Agent launch staged. Continue the task in the desktop client.",
"Agent orchestration": "Agent orchestration",
"Agent runtime model": "Agent runtime model",
"Agent runtime model id": "Agent runtime model ID",
"ago": "ago",
"AGPL v3.0 License": "AGPL v3.0 License",
"AI model testing environment": "AI model testing environment",
@@ -504,7 +504,7 @@
"Browse and compare": "Browse and compare",
"budget": "budget",
"Budget & usage": "Budget & usage",
"Budget caps": "Agnet runtime caps",
"Budget caps": "Agent runtime caps",
"Budget tokens = max tokens × ratio. Accepts a decimal between 0.002 and 1. Recommended to keep aligned with upstream billing.": "Budget tokens = max tokens × ratio. Accepts a decimal between 0.002 and 1. Recommended to keep aligned with upstream billing.",
"Budget tokens = max tokens × ratio. Accepts a decimal between 0.1 and 1.": "Budget tokens = max tokens × ratio. Accepts a decimal between 0.1 and 1.",
"Budget Tokens Ratio": "Budget Tokens Ratio",
@@ -547,7 +547,7 @@
"Cancelled": "Cancelled",
"Cancelled at": "Cancelled at",
"Capture a reusable bundle of models, tags, or endpoints.": "Capture a reusable bundle of models, tags, or endpoints.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, runtime caps, resource scope and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, runtime caps, resource scope and live status.",
"Category Name": "Category Name",
"Category name is required": "Category name is required",
"Category name must be less than 50 characters": "Category name must be less than 50 characters",
@@ -771,7 +771,7 @@
"Configure your account preferences and integrations": "Configure your account preferences and integrations",
"Confirm": "Confirm",
"Confirm Action": "Confirm Action",
"Confirm and launch Agnet": "Confirm and launch Agnet",
"Confirm and launch Agent": "Confirm and launch Agent",
"Confirm Batch Update": "Confirm Batch Update",
"Confirm Billing Conflicts": "Confirm Billing Conflicts",
"Confirm Changes": "Confirm Changes",
@@ -790,7 +790,7 @@
"Confirm password": "Confirm password",
"Confirm Payment": "Confirm Payment",
"Confirm recommendation summary": "Confirm recommendation summary",
"Confirm scope, risk and budget before launching Agnet": "Confirm scope, risk and budget before launching Agnet",
"Confirm scope, risk and budget before launching Agent": "Confirm scope, risk and budget before launching Agent",
"Confirm Selection": "Confirm Selection",
"Confirm settings and finish setup": "Confirm settings and finish setup",
"Confirm Unbind": "Confirm Unbind",
@@ -883,8 +883,8 @@
"Create a new code delivery run with checks.": "Create a new code delivery run with checks.",
"Create a new user group to configure ratio overrides for.": "Create a new user group to configure ratio overrides for.",
"Create account": "Create account",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the orchestration plan to Agnet. Model choices here are runtime policy, not billing setup.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the orchestration plan to Agent. Model choices here are runtime policy, not billing setup.",
"Create an account": "Create an account",
"Create and review invite or credit codes.": "Create and review invite or credit codes.",
"Create API Key": "Create API Key",
@@ -1040,7 +1040,7 @@
"Deployments": "Deployments",
"Desc": "Desc",
"Describe": "Describe",
"Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.": "Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.",
"Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agent team.": "Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agent team.",
"Describe this model...": "Describe this model...",
"Describe this vendor...": "Describe this vendor...",
"Describe what this work should achieve.": "Describe what this work should achieve.",
@@ -1181,7 +1181,7 @@
"e.g., us-central1 or JSON format for model-specific regions": "e.g., us-central1 or JSON format for model-specific regions",
"e.g., v2.1": "e.g., v2.1",
"Each backup code can only be used once.": "Each backup code can only be used once.",
"Each card maps one child Agnet role to a runtime model and one bounded resource grant.": "Each card maps one child Agnet role to a runtime model and one bounded resource grant.",
"Each card maps one child Agent role to a runtime model and one bounded resource grant.": "Each card maps one child Agent role to a runtime model and one bounded resource grant.",
"Each item must be an object with a single key-value pair.": "Each item must be an object with a single key-value pair.",
"Each item must have exactly one key-value pair.": "Each item must have exactly one key-value pair.",
"Each line represents one keyword. Leave blank to disable the list but keep the switch states.": "Each line represents one keyword. Leave blank to disable the list but keep the switch states.",
@@ -1677,8 +1677,8 @@
"Get Started": "Get Started",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) that Resource Grants allow this run to draw from.",
"Git sources workflow step 3": "Allocate cloud capacity and permissions for sub-agents—for example dedicated VMs, roles, and API scopes.",
@@ -1958,7 +1958,7 @@
"Last updated:": "Last updated:",
"Last Used": "Last Used",
"Last used:": "Last used:",
"Launch Agnet": "Launch Agnet",
"Launch Agent": "Launch Agent",
"Launching…": "Launching…",
"Layout": "Layout",
"Learn more": "Learn more",
@@ -1978,7 +1978,7 @@
"Leave empty to use system temp directory": "Leave empty to use system temp directory",
"Leave empty to use username": "Leave empty to use username",
"Less": "Less",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "Light",
"Lightning Fast": "Lightning Fast",
"Limit period": "Limit period",
@@ -2023,7 +2023,7 @@
"Logo": "Logo",
"Logo URL": "Logo URL",
"Logs": "Logs",
"Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.",
"Long-lived credentials are stored in the secret vault. Agent only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "Long-lived credentials are stored in the secret vault. Agent only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.",
"m": "m",
"Maintain a list of common questions for the dashboard help panel": "Maintain a list of common questions for the dashboard help panel",
"Maintenance": "Maintenance",
@@ -2381,7 +2381,7 @@
"No Sync": "No Sync",
"No system announcements": "No system announcements",
"No tasks yet": "No tasks yet",
"No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.": "No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.",
"No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agent.": "No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agent.",
"No token found.": "No token found.",
"No tools configured": "No tools configured",
"No Upgrade": "No Upgrade",
@@ -3039,7 +3039,7 @@
"Resources workflow step 1": "Bind project code, SK, document, or cloud-resource metadata without exposing raw credentials.",
"Resources workflow step 2": "Keep secret material behind secret_ref; Manager surfaces references and status only.",
"Resources workflow step 3": "Allocate scope, allowed paths/actions, runtime policy, and budget to the run manifest.",
"Resources workflow step 4": "Start a Work/Run; Agnet resolves immutable anchors and enforces the effective grants.",
"Resources workflow step 4": "Start a Work/Run; Agent resolves immutable anchors and enforces the effective grants.",
"Resources workflow step 5": "Use snapshots, events, and audit together to replay which resource context actually ran.",
"Resources workflow title": "Resource-to-run flow",
"Response": "Response",
@@ -3295,7 +3295,7 @@
"Sidebar Personal Settings": "Sidebar Personal Settings",
"Sign in": "Sign in",
"Sign In": "Sign In",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Sign in with Passkey",
@@ -3364,10 +3364,10 @@
"status": "status",
"Status & Sync": "Status & Sync",
"Status Code Mapping": "Status Code Mapping",
"Status of every Agnet task you launched": "Status of every Agnet task you launched",
"Status of every Agent task you launched": "Status of every Agent task you launched",
"Status Page Slug": "Status Page Slug",
"Status, errors and budget burn in one auditable stream.": "Status, errors and budget burn in one auditable stream.",
"Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.": "Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.",
"Status, latest activity and last update for every Agent task you launched. Details live in the desktop client.": "Status, latest activity and last update for every Agent task you launched. Details live in the desktop client.",
"Status:": "Status:",
"Stay": "Stay",
"Stay tuned though!": "Stay tuned though!",
@@ -3480,7 +3480,7 @@
"Task Logs": "Task Logs",
"Task not found. It may have been removed or was never created.": "Task not found. It may have been removed or was never created.",
"Task overview": "Task overview",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agent.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agent.",
"Runtime diagnostics": "Runtime diagnostics",
"Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.": "Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.",
"runtime mode": "runtime mode",
@@ -3685,7 +3685,7 @@
"Total:": "Total:",
"TPM": "TPM",
"Trace delivery context snapshots by hash.": "Trace delivery context snapshots by hash.",
"Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.": "Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.",
"Track every Agent work run by status, risk, budget, scope and secret_ref coverage.": "Track every Agent work run by status, risk, budget, scope and secret_ref coverage.",
"Track per-request consumption to power usage analytics. Keeping this on increases database writes.": "Track per-request consumption to power usage analytics. Keeping this on increases database writes.",
"Track usage, costs and performance with real-time analytics": "Track usage, costs and performance with real-time analytics",
"Tracks current account base limits and additional metered usage on Codex upstream.": "Tracks current account base limits and additional metered usage on Codex upstream.",
@@ -4112,7 +4112,7 @@
"This run will do": "This run will do",
"No objective provided yet — go back to the Idea tab.": "No objective provided yet — go back to the Idea tab.",
"Resources this run may use": "Resources this run may use",
"No resources bound yet — Agnet will run with no external data access.": "No resources bound yet — Agnet will run with no external data access.",
"No resources bound yet — Agent will run with no external data access.": "No resources bound yet — Agent will run with no external data access.",
"no actions specified": "no actions specified",
"This run will NOT do": "This run will NOT do",
"Production deploys without client approval": "Production deploys without client approval",
@@ -4143,7 +4143,7 @@
"No audit events yet for this deployment.": "No audit events yet for this deployment.",
"Connect project docs": "Connect project docs",
"{{n}} doc source connected": "{{n}} doc source connected",
"Link product requirements, design docs or wiki repos so Agnet has project context.": "Link product requirements, design docs or wiki repos so Agnet has project context.",
"Link product requirements, design docs or wiki repos so Agent has project context.": "Link product requirements, design docs or wiki repos so Agent has project context.",
"Connect SK skill packs": "Connect SK skill packs",
"{{n}} SK source connected": "{{n}} SK source connected",
"Pick a reusable skill / agent toolset repository, or skip.": "Pick a reusable skill / agent toolset repository, or skip.",
+9 -9
View File
@@ -211,12 +211,12 @@
"Inherits deployment defaults": "Inherits deployment defaults",
"Add agent": "Add agent",
"Agent": "Agent",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Allowed models comma": "Allowed model IDs (comma-separated)",
"Budget caps": "Budget caps",
"Cloud principals comma": "Cloud principals (comma-separated)",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agnet.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agent.",
"Default model id": "Default model ID",
"Deployment plan": "Deployment plan",
"Deployment request failed": "Deployment request failed",
@@ -236,7 +236,7 @@
"Aggregated usage metrics and trend charts.": "Métriques d'utilisation agrégées et graphiques de tendances.",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "agrège plus de 50 fournisseurs IA derrière une API unifiée. Gérez l'accès, suivez les coûts et évoluez sans effort.",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AGPL v3.0 License": "Licence AGPL v3.0",
"AI model testing environment": "Environnement de test de modèle IA",
"AI models": "Modèles d'IA",
@@ -566,7 +566,7 @@
"Cancelled": "Annulé",
"Cancelled at": "Annulé le",
"Capture a reusable bundle of models, tags, or endpoints.": "Capturez un ensemble réutilisable de modèles, d'étiquettes ou de points de terminaison.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Category Name": "Nom de la catégorie",
"Category name is required": "Le nom de la catégorie est requis",
"Category name must be less than 50 characters": "Le nom de la catégorie doit contenir moins de 50 caractères",
@@ -1640,8 +1640,8 @@
"Get Started": "Commencer",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow title": "Typical setup flow",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) your tenant is allowed to draw from.",
@@ -1918,7 +1918,7 @@
"Leave empty to use system temp directory": "Laisser vide pour utiliser le répertoire temporaire",
"Leave empty to use username": "Laissez vide pour utiliser le nom d'utilisateur",
"Less": "Moins",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "Clair",
"Lightning Fast": "Extrêmement rapide",
"Limit period": "Période de limite",
@@ -3146,7 +3146,7 @@
"Sidebar Personal Settings": "Paramètres personnels de la barre latérale",
"Sign in": "Se connecter",
"Sign In": "Se connecter",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Se connecter avec Passkey",
+9 -9
View File
@@ -211,12 +211,12 @@
"Inherits deployment defaults": "Inherits deployment defaults",
"Add agent": "Add agent",
"Agent": "Agent",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Allowed models comma": "Allowed model IDs (comma-separated)",
"Budget caps": "Budget caps",
"Cloud principals comma": "Cloud principals (comma-separated)",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agnet.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agent.",
"Default model id": "Default model ID",
"Deployment plan": "Deployment plan",
"Deployment request failed": "Deployment request failed",
@@ -236,7 +236,7 @@
"Aggregated usage metrics and trend charts.": "集計された使用量メトリクスとトレンドチャート。",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "50以上のAIプロバイダーを統一APIで集約。アクセス管理、コスト追跡、スケーリングを簡単に。",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AGPL v3.0 License": "AGPL v3.0ライセンス",
"AI model testing environment": "AIモデルテスト環境",
"AI models": "AIモデル",
@@ -566,7 +566,7 @@
"Cancelled": "キャンセル",
"Cancelled at": "キャンセル日時",
"Capture a reusable bundle of models, tags, or endpoints.": "モデル、タグ、またはエンドポイントの再利用可能なバンドルを保存。",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Category Name": "分類名称",
"Category name is required": "カテゴリ名は必須です",
"Category name must be less than 50 characters": "カテゴリ名は50文字以内にしてください",
@@ -1640,8 +1640,8 @@
"Get Started": "開始する",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow title": "Typical setup flow",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) your tenant is allowed to draw from.",
@@ -1918,7 +1918,7 @@
"Leave empty to use system temp directory": "空欄でシステムの一時ディレクトリを使用",
"Leave empty to use username": "ユーザー名を使用するには空のままにしてください",
"Less": "少ない",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "ライト",
"Lightning Fast": "超高速",
"Limit period": "制限期間",
@@ -3146,7 +3146,7 @@
"Sidebar Personal Settings": "サイドバー個人設定",
"Sign in": "ログイン",
"Sign In": "ログイン",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Passkeyでログイン",
+9 -9
View File
@@ -211,12 +211,12 @@
"Inherits deployment defaults": "Inherits deployment defaults",
"Add agent": "Add agent",
"Agent": "Agent",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Allowed models comma": "Allowed model IDs (comma-separated)",
"Budget caps": "Budget caps",
"Cloud principals comma": "Cloud principals (comma-separated)",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agnet.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agent.",
"Default model id": "Default model ID",
"Deployment plan": "Deployment plan",
"Deployment request failed": "Deployment request failed",
@@ -236,7 +236,7 @@
"Aggregated usage metrics and trend charts.": "Агрегированные метрики использования и графики трендов.",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "объединяет 50+ ИИ-провайдеров за единым API. Управляйте доступом, отслеживайте затраты и масштабируйтесь без усилий.",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AGPL v3.0 License": "Лицензия AGPL v3.0",
"AI model testing environment": "Среда тестирования ИИ моделей",
"AI models": "Модели ИИ",
@@ -566,7 +566,7 @@
"Cancelled": "Отменено",
"Cancelled at": "Отменено",
"Capture a reusable bundle of models, tags, or endpoints.": "Создайте повторно используемый набор моделей, тегов или конечных точек.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Category Name": "Название категории",
"Category name is required": "Название категории обязательно",
"Category name must be less than 50 characters": "Название категории должно содержать менее 50 символов",
@@ -1640,8 +1640,8 @@
"Get Started": "Начать",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow title": "Typical setup flow",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) your tenant is allowed to draw from.",
@@ -1918,7 +1918,7 @@
"Leave empty to use system temp directory": "Оставьте пустым для системного временного каталога",
"Leave empty to use username": "Оставьте пустым, чтобы использовать имя пользователя",
"Less": "Меньше",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "Светлая",
"Lightning Fast": "Молниеносно быстро",
"Limit period": "Период ограничения",
@@ -3146,7 +3146,7 @@
"Sidebar Personal Settings": "Личные настройки боковой панели",
"Sign in": "Войти",
"Sign In": "Войти",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Войти с Passkey",
+9 -9
View File
@@ -211,12 +211,12 @@
"Inherits deployment defaults": "Inherits deployment defaults",
"Add agent": "Add agent",
"Agent": "Agent",
"Agnet deployment created": "Agnet deployment created ({{deployment_id}})",
"Agent deployment created": "Agent deployment created ({{deployment_id}})",
"Allowed models comma": "Allowed model IDs (comma-separated)",
"Budget caps": "Budget caps",
"Cloud principals comma": "Cloud principals (comma-separated)",
"Create Agnet deployment": "Create Agnet deployment",
"Create Agnet deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agnet.",
"Create Agent deployment": "Create Agent deployment",
"Create Agent deployment description": "Send the full orchestration plan in one request. Manager stores it; execution runs on Agent.",
"Default model id": "Default model ID",
"Deployment plan": "Deployment plan",
"Deployment request failed": "Deployment request failed",
@@ -236,7 +236,7 @@
"Aggregated usage metrics and trend charts.": "Chỉ số sử dụng tổng hợp và biểu đồ xu hướng.",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "tổng hợp hơn 50 nhà cung cấp AI sau một API thống nhất. Quản lý truy cập, theo dõi chi phí và mở rộng dễ dàng.",
"Agile Minimal": "Agile Minimal",
"Agnet orchestration": "Agnet orchestration",
"Agent orchestration": "Agent orchestration",
"AGPL v3.0 License": "Giấy phép AGPL v3.0",
"AI model testing environment": "Môi trường thử nghiệm mô hình AI",
"AI models": "mô hình AI",
@@ -566,7 +566,7 @@
"Cancelled": "Đã hủy",
"Cancelled at": "Đã hủy lúc",
"Capture a reusable bundle of models, tags, or endpoints.": "Đóng gói một bộ có thể tái sử dụng gồm các mô hình, thẻ hoặc điểm cuối.",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "Card-based view of every Agnet orchestration run with risk, budget, executor and live status.",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "Card-based view of every Agent orchestration run with risk, budget, executor and live status.",
"Category Name": "Tên danh mục",
"Category name is required": "Tên danh mục là bắt buộc",
"Category name must be less than 50 characters": "Tên danh mục phải ít hơn 50 ký tự",
@@ -1640,8 +1640,8 @@
"Get Started": "Bắt đầu",
"Git binding": "Git binding",
"Git sources": "Git sources",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agnet, then review snapshot anchors per deployment (enforcement lives on Agnet).",
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agent when it starts each deployment; effective permissions are stored and enforced on Agent. Below lists immutable snapshot anchors (Git commit / upload artifact) Agent resolved for auditing.",
"Git sources subtitle": "Configure bindings and deployment parameters for Agent, then review snapshot anchors per deployment (enforcement lives on Agent).",
"Git sources workflow title": "Typical setup flow",
"Git sources workflow step 1": "Bind your team’s Git repositories that hold application code and delivery context.",
"Git sources workflow step 2": "Bind SK tool repositories (skills registry) your tenant is allowed to draw from.",
@@ -1918,7 +1918,7 @@
"Leave empty to use system temp directory": "Để trống để sử dụng thư mục tạm của hệ thống",
"Leave empty to use username": "Để trống để sử dụng tên người dùng",
"Less": "Ít hơn",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.",
"Light": "Ánh sáng",
"Lightning Fast": "Nhanh như chớp",
"Limit period": "Thời hiệu",
@@ -3146,7 +3146,7 @@
"Sidebar Personal Settings": "Cài đặt cá nhân thanh bên",
"Sign in": "Đăng nhập",
"Sign In": "Đăng nhập",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "Sign in to bind repositories and cloud permissions, deploy sub-agents, and review Git-bound snapshot anchors for tenants under your account.",
"Sign in to your workspace": "Sign in to your workspace",
"Sign in with Passkey": "Đăng nhập bằng Passkey",
+38 -38
View File
@@ -78,7 +78,7 @@
"Actions": "操作",
"active": "活跃",
"Active": "生效",
"Active Agnet sub-loops": "正在推进的 Agnet 子环节",
"Active Agent sub-loops": "正在推进的 Agent 子环节",
"Active Cache Count": "活跃缓存数",
"Active code delivery runs across tenants": "当前各租户正在进行的代码交付任务",
"Active Files": "活跃文件",
@@ -176,20 +176,20 @@
"After invalidating, this subscription will be immediately deactivated. Historical records are not affected. Continue?": "作废后该订阅将立即失效,历史记录不受影响。是否继续?",
"After scanning, the binding will complete automatically": "扫描后,绑定将自动完成",
"Agent": "子 Agent",
"Agent declarations parsed from each Agnet deployment plan.": "从各 Agnet 部署计划中解析的 Agent 声明。",
"Agent declarations parsed from each deployment plan.": "从各 Agnet 部署计划中解析的 Agent 声明。",
"Agent declarations parsed from each Agent deployment plan.": "从各 Agent 部署计划中解析的 Agent 声明。",
"Agent declarations parsed from each deployment plan.": "从各 Agent 部署计划中解析的 Agent 声明。",
"Agent ID *": "代理 ID *",
"Agentic development control plane": "智能体研发控制面",
"Agents": "Agent",
"Aggregated usage metrics and trend charts.": "聚合使用指标和趋势图表。",
"aggregates 50+ AI providers behind one unified API. Manage access, track costs, and scale effortlessly.": "聚合 50+ AI 提供商于统一 API 之后。轻松管理访问、追踪成本、弹性扩展。",
"Agile Minimal": "敏捷最小编队",
"Agnet allowed models comma": "Agnet 运行模型 ID(逗号分隔)",
"Agnet deployment created": "Agnet 部署已创建({{deployment_id}})",
"Agnet launch staged. Continue the task in the desktop client.": "已准备启动 Agnet,请回到客户端继续推进任务。",
"Agnet orchestration": "Agnet 编排",
"Agnet runtime model": "Agnet 运行模型",
"Agnet runtime model id": "Agnet 运行模型 ID",
"Agent allowed models comma": "Agent 运行模型 ID(逗号分隔)",
"Agent deployment created": "Agent 部署已创建({{deployment_id}})",
"Agent launch staged. Continue the task in the desktop client.": "已准备启动 Agent,请回到客户端继续推进任务。",
"Agent orchestration": "Agent 编排",
"Agent runtime model": "Agent 运行模型",
"Agent runtime model id": "Agent 运行模型 ID",
"ago": "前",
"AGPL v3.0 License": "AGPL v3.0 协议",
"AI model testing environment": "AI模型测试环境",
@@ -505,7 +505,7 @@
"Browse and compare": "浏览和比较",
"budget": "预算",
"Budget & usage": "预算与用量",
"Budget caps": "Agnet 运行上限",
"Budget caps": "Agent 运行上限",
"Budget tokens = max tokens × ratio. Accepts a decimal between 0.002 and 1. Recommended to keep aligned with upstream billing.": "预算令牌 = 最大令牌数 × 比例。接受 0.002 到 1 之间的十进制数。建议与上游计费保持一致。",
"Budget tokens = max tokens × ratio. Accepts a decimal between 0.1 and 1.": "预算令牌 = 最大令牌数 × 比例。接受 0.1 到 1 之间的十进制数。",
"Budget Tokens Ratio": "预算令牌比例",
@@ -548,7 +548,7 @@
"Cancelled": "已取消",
"Cancelled at": "作废于",
"Capture a reusable bundle of models, tags, or endpoints.": "捕获可重用的模型、标签或端点捆绑包。",
"Card-based view of every Agnet orchestration run with risk, budget, executor and live status.": "以卡片形式展示每次 Agnet 编排运行,包含风险、运行上限、资源作用域与实时状态。",
"Card-based view of every Agent orchestration run with risk, budget, executor and live status.": "以卡片形式展示每次 Agent 编排运行,包含风险、运行上限、资源作用域与实时状态。",
"Category Name": "分类名称",
"Category name is required": "分类名称不能为空",
"Category name must be less than 50 characters": "分类名称不能超过 50 个字符",
@@ -772,7 +772,7 @@
"Configure your account preferences and integrations": "配置您的账户偏好和集成",
"Confirm": "确认",
"Confirm Action": "确认操作",
"Confirm and launch Agnet": "确认并启动 Agnet",
"Confirm and launch Agent": "确认并启动 Agent",
"Confirm Batch Update": "确认批量更新",
"Confirm Billing Conflicts": "确认账单冲突",
"Confirm Changes": "确认更改",
@@ -791,7 +791,7 @@
"Confirm password": "确认密码",
"Confirm Payment": "确认付款",
"Confirm recommendation summary": "确认推荐摘要",
"Confirm scope, risk and budget before launching Agnet": "在启动 Agnet 之前,确认范围、风险和预算",
"Confirm scope, risk and budget before launching Agent": "在启动 Agent 之前,确认范围、风险和预算",
"Confirm Selection": "确认选择",
"Confirm settings and finish setup": "确认设置并完成安装",
"Confirm Unbind": "确认解绑",
@@ -885,8 +885,8 @@
"Create a new code delivery run with checks.": "创建新的带检查项的代码交付任务。",
"Create a new user group to configure ratio overrides for.": "创建一个新的用户分组来配置比例覆盖。",
"Create account": "创建账户",
"Create Agnet deployment": "创建 Agnet 部署",
"Create Agnet deployment description": "向 Agnet 提交编排计划。这里的模型选择是运行策略,不是计费配置。",
"Create Agent deployment": "创建 Agent 部署",
"Create Agent deployment description": "向 Agent 提交编排计划。这里的模型选择是运行策略,不是计费配置。",
"Create an account": "创建一个账户",
"Create and review invite or credit codes.": "创建和审查邀请或信用代码。",
"Create API Key": "创建 API 密钥",
@@ -1042,7 +1042,7 @@
"Deployments": "部署",
"Desc": "描述",
"Describe": "图生文",
"Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agnet team.": "用自然语言描述产品、功能或修复点。Heicode 会生成任务草案、推荐需要的资源,并调度 Agnet 团队执行。",
"Describe the product, feature, or fix. Heicode will draft the task, recommend resources, and dispatch the Agent team.": "用自然语言描述产品、功能或修复点。Heicode 会生成任务草案、推荐需要的资源,并调度 Agent 团队执行。",
"Describe this model...": "描述此模型...",
"Describe this vendor...": "描述此供应商...",
"Describe what this work should achieve.": "描述这次工作要达成什么。",
@@ -1183,7 +1183,7 @@
"e.g., us-central1 or JSON format for model-specific regions": "例如,us-central1 或模型特定区域的 JSON 格式",
"e.g., v2.1": "例如,v2.1",
"Each backup code can only be used once.": "每个备份代码只能使用一次。",
"Each card maps one child Agnet role to a runtime model and one bounded resource grant.": "每张卡把一个子 Agnet 角色映射到运行模型和一个受限资源授权。",
"Each card maps one child Agent role to a runtime model and one bounded resource grant.": "每张卡把一个子 Agent 角色映射到运行模型和一个受限资源授权。",
"Each item must be an object with a single key-value pair.": "每个条目必须是包含单个键值对的对象。",
"Each item must have exactly one key-value pair.": "每个条目必须恰好包含一个键值对。",
"Each line represents one keyword. Leave blank to disable the list but keep the switch states.": "每行代表一个关键词。留空以禁用列表,但保留开关状态。",
@@ -1679,8 +1679,8 @@
"Get Started": "开始使用",
"Git binding": "Git 绑定",
"Git sources": "Git 来源",
"Git sources binding explainer": "Skill(SK)定义以 Git 为唯一事实源。Manager 不在此编辑 Markdown:请在 Heicode 客户端或部署计划的 sk_sources 中登记仓库与引用;运行时与 SK 策略等参数在 Agnet 拉起编队/子 Agent 时传入。有效权限与策略落账在 Agnet 侧并由其执行;Manager 仅展示 Agnet 回传的不可变快照锚点(Git commit / 上传制品)供审计。",
"Git sources subtitle": "在部署参数中把绑定与策略交给 Agnet,在此按部署查看快照锚点(执行权在 Agnet)。",
"Git sources binding explainer": "Skill(SK)定义以 Git 为唯一事实源。Manager 不在此编辑 Markdown:请在 Heicode 客户端或部署计划的 sk_sources 中登记仓库与引用;运行时与 SK 策略等参数在 Agent 拉起编队/子 Agent 时传入。有效权限与策略落账在 Agent 侧并由其执行;Manager 仅展示 Agent 回传的不可变快照锚点(Git commit / 上传制品)供审计。",
"Git sources subtitle": "在部署参数中把绑定与策略交给 Agent,在此按部署查看快照锚点(执行权在 Agent)。",
"Git sources workflow step 1": "绑定团队用于应用代码与交付上下文的 Git 仓库。",
"Git sources workflow step 2": "绑定 SK 工具仓库(技能来源),声明本次运行可通过 Resource Grant 引用的工具集。",
"Git sources workflow step 3": "为子 Agent 分配云上资源与权限(如独立虚拟机、角色与其他访问边界)。",
@@ -1960,7 +1960,7 @@
"Last updated:": "上次更新时间:",
"Last Used": "最后使用时间",
"Last used:": "上次使用时间:",
"Launch Agnet": "启动 Agnet",
"Launch Agent": "启动 Agent",
"Launching…": "正在准备启动…",
"Layout": "布局",
"Learn more": "了解更多",
@@ -1980,7 +1980,7 @@
"Leave empty to use system temp directory": "留空使用系统临时目录",
"Leave empty to use username": "留空以使用用户名",
"Less": "更少",
"Lifecycle and policy events emitted by Agnet deployments. Correlate with deployment cards.": "Agnet 部署发出的生命周期与策略事件,可与部署卡片关联。",
"Lifecycle and policy events emitted by Agent deployments. Correlate with deployment cards.": "Agent 部署发出的生命周期与策略事件,可与部署卡片关联。",
"Light": "浅色",
"Lightning Fast": "极速",
"Limit period": "限制周期",
@@ -2025,7 +2025,7 @@
"Logo": "徽标",
"Logo URL": "徽标 URL",
"Logs": "日志",
"Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "长期凭证保存在密钥保管器。Agnet 执行时只申请短期、最小权限凭证。生产部署等高危操作需在客户端审批。",
"Long-lived credentials are stored in the secret vault. Agent only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "长期凭证保存在密钥保管器。Agent 执行时只申请短期、最小权限凭证。生产部署等高危操作需在客户端审批。",
"m": "分钟",
"Maintain a list of common questions for the dashboard help panel": "维护仪表板帮助面板的常见问题列表",
"Maintenance": "维护",
@@ -2385,7 +2385,7 @@
"No Sync": "不同步",
"No system announcements": "暂无系统公告",
"No tasks yet": "暂无任务",
"No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agnet.": "暂无任务。在上方暂存一个想法,然后在客户端确认推荐摘要,启动 Agnet。",
"No tasks yet. Capture an idea above, then confirm the recommendation in the desktop client to launch Agent.": "暂无任务。在上方暂存一个想法,然后在客户端确认推荐摘要,启动 Agent。",
"No token found.": "未找到令牌。",
"No tools configured": "未配置工具",
"No Upgrade": "不升级",
@@ -3043,7 +3043,7 @@
"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 4": "启动 Work/Run;Agent 解析不可变锚点并执行有效授权。",
"Resources workflow step 5": "结合快照、事件和审计回放实际运行的资源上下文。",
"Resources workflow title": "资源到运行流程",
"Response": "响应",
@@ -3299,7 +3299,7 @@
"Sidebar Personal Settings": "左侧边栏个人设置",
"Sign in": "登录",
"Sign In": "登录",
"Sign in to operate Agnet deployments, inspect events, and audit SK snapshots for every tenant under your account.": "登录后即可操作 Agnet 部署、查看事件,并审计你账号下各租户的 SK 快照。",
"Sign in to operate Agent deployments, inspect events, and audit SK snapshots for every tenant under your account.": "登录后即可操作 Agent 部署、查看事件,并审计你账号下各租户的 SK 快照。",
"Sign in to operate deployments and review Git-bound snapshot anchors for tenants under your account.": "登录后即可绑定仓库与云上权限、部署子 Agent,并审阅你账号下各租户与 Git 绑定的快照锚点。",
"Sign in to your workspace": "登录到你的工作空间",
"Sign in with Passkey": "使用 Passkey 登录",
@@ -3368,10 +3368,10 @@
"status": "状态",
"Status & Sync": "状态与同步",
"Status Code Mapping": "状态码映射",
"Status of every Agnet task you launched": "查看你启动的每个 Agnet 任务的状态",
"Status of every Agent task you launched": "查看你启动的每个 Agent 任务的状态",
"Status Page Slug": "状态页面 Slug",
"Status, errors and budget burn in one auditable stream.": "状态、错误与预算消耗汇聚在一条可审计的流里。",
"Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.": "查看每个启动的 Agnet 任务的状态、最近动态和更新时间。详细对话与产物在客户端中查看。",
"Status, latest activity and last update for every Agent task you launched. Details live in the desktop client.": "查看每个启动的 Agent 任务的状态、最近动态和更新时间。详细对话与产物在客户端中查看。",
"Status:": "状态:",
"Stay": "留下来",
"Stay tuned though!": "敬请期待!",
@@ -3484,7 +3484,7 @@
"Task Logs": "任务日志",
"Task not found. It may have been removed or was never created.": "任务不存在,可能已被删除或从未创建。",
"Task overview": "任务总览",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "在客户端确认推荐摘要并启动 Agnet 后,任务会出现在这里。",
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agent.": "在客户端确认推荐摘要并启动 Agent 后,任务会出现在这里。",
"Runtime diagnostics": "运行时诊断",
"Manager checks Runtime status separately from callback data, without mixing ordinary sub and swarm modes.": "Manager 会独立检查运行时状态,并明确区分普通 sub 与蜂群模式,不混用回调数据。",
"runtime mode": "运行模式",
@@ -3685,7 +3685,7 @@
"Total:": "总计:",
"TPM": "TPM",
"Trace delivery context snapshots by hash.": "按哈希追溯交付上下文快照。",
"Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.": "按状态、风险、预算、作用域和 secret_ref 覆盖情况跟踪每次 Agnet 工作运行。",
"Track every Agent work run by status, risk, budget, scope and secret_ref coverage.": "按状态、风险、预算、作用域和 secret_ref 覆盖情况跟踪每次 Agent 工作运行。",
"Track per-request consumption to power usage analytics. Keeping this on increases database writes.": "跟踪每个请求的消耗,以支持使用情况分析。保持开启会增加数据库写入。",
"Track usage, costs and performance with real-time analytics": "通过实时分析跟踪用量、成本和性能",
"Tracks current account base limits and additional metered usage on Codex upstream.": "跟踪当前账号在 Codex 上游的基础限额与附加计费用量。",
@@ -4139,11 +4139,11 @@
"Reviewer role": "评审",
"Ops role": "运维",
"Builder role": "构建",
"Product Agnet": "产品智能体",
"Frontend Agnet": "前端智能体",
"Backend Agnet": "后端智能体",
"Reviewer Agnet": "评审智能体",
"Ops Agnet": "运维智能体",
"Product Agent": "产品智能体",
"Frontend Agent": "前端智能体",
"Backend Agent": "后端智能体",
"Reviewer Agent": "评审智能体",
"Ops Agent": "运维智能体",
"Git repository": "Git 仓库",
"SK skill pack": "SK 技能包",
"Cloud account": "云账号",
@@ -4179,7 +4179,7 @@
"All levels": "全部级别",
"Info": "信息",
"Active credential leases": "有效凭证租约",
"Approve or reject high-risk Agnet operations before credentials are leased.": "在下发凭证租约前,审批或拒绝高危 Agnet 操作。",
"Approve or reject high-risk Agent operations before credentials are leased.": "在下发凭证租约前,审批或拒绝高危 Agent 操作。",
"No pending approvals": "暂无待审批项",
"High-risk operations will appear here.": "高危操作会显示在这里。",
"No active credential leases": "暂无有效凭证租约",
@@ -4195,14 +4195,14 @@
"Approved from Manager audit page": "从管理端审计页批准",
"Rejected from Manager audit page": "从管理端审计页拒绝",
"Revoked from Manager audit page": "从管理端审计页撤销",
"Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agnet.": "为当前任务绑定代码、文档和云资源,然后确认推荐方案再启动 Agnet。",
"Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agent.": "为当前任务绑定代码、文档和云资源,然后确认推荐方案再启动 Agent。",
"Plaintext credentials are never shown. The secret_ref column is a vault pointer, not the secret itself.": "永远不展示明文密钥。表中的密钥引用只是密钥保管器里的指针,不是密钥本体。",
"Confirm before launch": "确认后启动",
"Heicode summarises what this run will do, what resources it can use, what stays off-limits, and the expected cost. Confirm to launch.": "Heicode 已汇总本次会做什么、能用哪些资源、哪些事情不会做、以及预计消耗。请确认后启动。",
"This run will do": "本次会做",
"No objective provided yet — go back to the Idea tab.": "还没填写目标 — 请回到「想法」标签页补充。",
"Resources this run may use": "本次允许使用的资源",
"No resources bound yet — Agnet will run with no external data access.": "还没绑定资源 — Agnet 将在无外部数据访问的情况下运行。",
"No resources bound yet — Agent will run with no external data access.": "还没绑定资源 — Agent 将在无外部数据访问的情况下运行。",
"no actions specified": "未指定动作",
"This run will NOT do": "本次不会做",
"Production deploys without client approval": "未经客户端审批的生产部署",
@@ -4233,7 +4233,7 @@
"No audit events yet for this deployment.": "该部署暂无审计事件。",
"Connect project docs": "绑定项目文档",
"{{n}} doc source connected": "已绑定 {{n}} 份项目文档",
"Link product requirements, design docs or wiki repos so Agnet has project context.": "绑定需求文档、设计文档或 wiki 仓库,让 Agnet 拿到项目上下文。",
"Link product requirements, design docs or wiki repos so Agent has project context.": "绑定需求文档、设计文档或 wiki 仓库,让 Agent 拿到项目上下文。",
"Connect SK skill packs": "绑定 SK 技能包",
"{{n}} SK source connected": "已绑定 {{n}} 个 SK 来源",
"Pick a reusable skill / agent toolset repository, or skip.": "选一个可复用的技能包 / Agent 工具集仓库,也可以跳过。",
+1 -1
View File
@@ -79,7 +79,7 @@ api.interceptors.response.use(
const url = String(error?.config?.url || '')
// Only treat 401 on identity/self endpoints as a real session expiry.
// Admin-only endpoints (/api/agnet, /api/channel, etc.) returning 401 for
// Admin-only endpoints (/api/agent, /api/channel, etc.) returning 401 for
// non-admin users should NOT reset the session.
const isIdentityEndpoint =
url.includes('/api/user/self') ||
+11 -11
View File
@@ -15,12 +15,12 @@ import { api } from '@/lib/api'
* §2 /api/resources/* ResourceBinding (5)
* §3 /api/resource-grants/* ResourceGrant (4)
* §4 /api/user/heicode/* NewAPI metadata passthrough (4)
* §5 /api/agnet/* Agnet platform stub (12 mock endpoints)
* §5 /api/agent/* Agent platform stub (12 mock endpoints)
* §6 /api/user/tasks/* HeicodeTask orchestration (5)
*
* The shapes below are what mcp-server actually returns — they intentionally
* differ from the Heicode-local /api/agnet/* shapes used in earlier UI work.
* That earlier work treated AgnetDeployment as the user-facing task object;
* differ from the Heicode-local /api/agent/* shapes used in earlier UI work.
* That earlier work treated AgentDeployment as the user-facing task object;
* the contract document is clear that HeicodeTask (§6) is the right object.
*/
@@ -227,7 +227,7 @@ export async function createDeploymentDraftFromHeicodeTask(
options: HeicodeTaskDeploymentDraftOptions = {}
): Promise<HeicodeTaskDeploymentDraft> {
const res = await api.post<Envelope<HeicodeTaskDeploymentDraft>>(
`/api/agnet/user/tasks/${encodeURIComponent(task.id)}/deployment-draft`,
`/api/agent/user/tasks/${encodeURIComponent(task.id)}/deployment-draft`,
{
task,
sub_mode: options.sub_mode ?? 'agile',
@@ -339,10 +339,10 @@ export async function getHeicodeLogs(
}
// =============================================================================
// §5 Agnet stub — only the endpoints UI needs.
// §5 Agent stub — only the endpoints UI needs.
// =============================================================================
export type McpAgnetDeployment = {
export type McpAgentDeployment = {
deployment_id: string
status: string
phase?: string
@@ -360,11 +360,11 @@ export type McpAgnetDeployment = {
}
}
export async function listMcpAgnetDeployments(params?: {
export async function listMcpAgentDeployments(params?: {
status?: string
limit?: number
binding_scope?: string
}): Promise<McpAgnetDeployment[]> {
}): Promise<McpAgentDeployment[]> {
const qs = new URLSearchParams()
if (params?.status) qs.set('status', params.status)
if (params?.limit != null) qs.set('limit', String(params.limit))
@@ -372,8 +372,8 @@ export async function listMcpAgnetDeployments(params?: {
const suffix = qs.toString() ? `?${qs.toString()}` : ''
try {
const env = await mcpFetch<
Envelope<{ items?: McpAgnetDeployment[]; total?: number }>
>(`/api/agnet/deployments${suffix}`)
Envelope<{ items?: McpAgentDeployment[]; total?: number }>
>(`/api/agent/deployments${suffix}`)
return env.data?.items ?? []
} catch {
return []
@@ -639,7 +639,7 @@ export async function listMcpAuditLogs(params?: {
try {
const env = await mcpFetch<
Envelope<{ items?: McpAuditEntry[]; total?: number }>
>(`/api/agnet/audit-logs${suffix}`)
>(`/api/agent/audit-logs${suffix}`)
return env.data?.items ?? []
} catch {
return []
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgnetAgentsPage } from '@/features/agnet-console/pages'
import { AgentAgentsPage } from '@/features/agent-console/pages'
export const Route = createFileRoute('/_authenticated/agents/')({
component: AgnetAgentsPage,
component: AgentAgentsPage,
})
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgnetAuditPage } from '@/features/agnet-console/pages'
import { AgentAuditPage } from '@/features/agent-console/pages'
export const Route = createFileRoute('/_authenticated/audit/')({
component: AgnetAuditPage,
component: AgentAuditPage,
})
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgnetDeploymentsPage } from '@/features/agnet-console/pages'
import { AgentDeploymentsPage } from '@/features/agent-console/pages'
export const Route = createFileRoute('/_authenticated/deployments/')({
component: AgnetDeploymentsPage,
component: AgentDeploymentsPage,
})
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgnetEventsPage } from '@/features/agnet-console/pages'
import { AgentEventsPage } from '@/features/agent-console/pages'
export const Route = createFileRoute('/_authenticated/events/')({
component: AgnetEventsPage,
component: AgentEventsPage,
})
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgnetSKSourcesPage } from '@/features/agnet-console/pages'
import { AgentSKSourcesPage } from '@/features/agent-console/pages'
export const Route = createFileRoute('/_authenticated/sk-sources/')({
component: AgnetSKSourcesPage,
component: AgentSKSourcesPage,
})
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgnetTemplatesPage } from '@/features/agnet-console/pages'
import { AgentTemplatesPage } from '@/features/agent-console/pages'
export const Route = createFileRoute('/_authenticated/templates/')({
component: AgnetTemplatesPage,
component: AgentTemplatesPage,
})
+1 -1
View File
@@ -2,7 +2,7 @@
* Heicode Manager design tokens
*
* One source of truth for radii, spacing, elevations, semantic colors and
* surface treatments used by the Agnet command-center UI. Pages and components
* surface treatments used by the Agent command-center UI. Pages and components
* should reference these tokens (via Tailwind or CSS variables) instead of
* declaring ad-hoc values.
* ==========================================================================*/