feat: add manager sub deployment bridge
This commit is contained in:
@@ -18,6 +18,9 @@ const (
|
|||||||
agnetRiskMedium = "medium"
|
agnetRiskMedium = "medium"
|
||||||
agnetRiskHigh = "high"
|
agnetRiskHigh = "high"
|
||||||
|
|
||||||
|
agnetSubModeAgile = "agile"
|
||||||
|
agnetSubModeWaterfall = "waterfall"
|
||||||
|
|
||||||
agnetResourceGit = "git"
|
agnetResourceGit = "git"
|
||||||
agnetResourceSK = "sk"
|
agnetResourceSK = "sk"
|
||||||
agnetResourceProjectDoc = "project_doc"
|
agnetResourceProjectDoc = "project_doc"
|
||||||
@@ -135,6 +138,7 @@ type agnetOrchestrationPlan struct {
|
|||||||
IntentID string `json:"intent_id"`
|
IntentID string `json:"intent_id"`
|
||||||
TemplateHint string `json:"template_hint"`
|
TemplateHint string `json:"template_hint"`
|
||||||
Objective string `json:"objective"`
|
Objective string `json:"objective"`
|
||||||
|
SubMode string `json:"sub_mode"`
|
||||||
RiskLevel string `json:"risk_level"`
|
RiskLevel string `json:"risk_level"`
|
||||||
Budget agnetBudget `json:"budget"`
|
Budget agnetBudget `json:"budget"`
|
||||||
UserContext agnetUserContext `json:"user_context"`
|
UserContext agnetUserContext `json:"user_context"`
|
||||||
@@ -151,6 +155,7 @@ type agnetDeploymentRequest struct {
|
|||||||
|
|
||||||
type agnetDeploymentRecord struct {
|
type agnetDeploymentRecord struct {
|
||||||
DeploymentID string `json:"deployment_id"`
|
DeploymentID string `json:"deployment_id"`
|
||||||
|
SubMode string `json:"sub_mode"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Phase string `json:"phase"`
|
Phase string `json:"phase"`
|
||||||
RuntimeState string `json:"runtime_state"`
|
RuntimeState string `json:"runtime_state"`
|
||||||
@@ -205,6 +210,10 @@ type agnetSKSnapshotResolveRequest struct {
|
|||||||
DeploymentID string `json:"deployment_id"`
|
DeploymentID string `json:"deployment_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type agnetSimulateDeploymentEventsRequest struct {
|
||||||
|
Events []string `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
type agnetSKSnapshot struct {
|
type agnetSKSnapshot struct {
|
||||||
SnapshotID string `json:"snapshot_id"`
|
SnapshotID string `json:"snapshot_id"`
|
||||||
DeploymentID string `json:"deployment_id"`
|
DeploymentID string `json:"deployment_id"`
|
||||||
@@ -290,6 +299,7 @@ func marshalAgnetSnapshot(v any) (string, error) {
|
|||||||
func agnetDeploymentModelToRecord(row model.AgnetDeployment) (agnetDeploymentRecord, error) {
|
func agnetDeploymentModelToRecord(row model.AgnetDeployment) (agnetDeploymentRecord, error) {
|
||||||
var record agnetDeploymentRecord
|
var record agnetDeploymentRecord
|
||||||
record.DeploymentID = row.DeploymentID
|
record.DeploymentID = row.DeploymentID
|
||||||
|
record.SubMode = normalizeAgnetSubMode(row.SubMode)
|
||||||
record.Status = row.Status
|
record.Status = row.Status
|
||||||
record.Phase = row.Phase
|
record.Phase = row.Phase
|
||||||
record.RuntimeState = row.RuntimeState
|
record.RuntimeState = row.RuntimeState
|
||||||
@@ -348,6 +358,7 @@ func persistAgnetDeploymentRecord(record agnetDeploymentRecord, req agnetDeploym
|
|||||||
ChannelID: record.Plan.UserContext.ChannelID,
|
ChannelID: record.Plan.UserContext.ChannelID,
|
||||||
BindingScope: firstPlanBindingScope(record.Plan),
|
BindingScope: firstPlanBindingScope(record.Plan),
|
||||||
CorrelationID: record.Plan.Metadata.CorrelationID,
|
CorrelationID: record.Plan.Metadata.CorrelationID,
|
||||||
|
SubMode: normalizeAgnetSubMode(record.SubMode),
|
||||||
Status: record.Status,
|
Status: record.Status,
|
||||||
Phase: record.Phase,
|
Phase: record.Phase,
|
||||||
RuntimeState: record.RuntimeState,
|
RuntimeState: record.RuntimeState,
|
||||||
@@ -380,6 +391,7 @@ func updateAgnetDeploymentRecord(record agnetDeploymentRecord) error {
|
|||||||
Where("deployment_id = ?", record.DeploymentID).
|
Where("deployment_id = ?", record.DeploymentID).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"status": record.Status,
|
"status": record.Status,
|
||||||
|
"sub_mode": normalizeAgnetSubMode(record.SubMode),
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
"runtime_state": record.RuntimeState,
|
"runtime_state": record.RuntimeState,
|
||||||
"failure_reason": record.FailureReason,
|
"failure_reason": record.FailureReason,
|
||||||
@@ -390,6 +402,24 @@ func updateAgnetDeploymentRecord(record agnetDeploymentRecord) error {
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeAgnetSubMode(value string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
|
case agnetSubModeWaterfall:
|
||||||
|
return agnetSubModeWaterfall
|
||||||
|
default:
|
||||||
|
return agnetSubModeAgile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidAgnetSubMode(value string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
|
case "", agnetSubModeAgile, agnetSubModeWaterfall:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func findAgnetDeploymentRecord(deploymentID string) (agnetDeploymentRecord, bool) {
|
func findAgnetDeploymentRecord(deploymentID string) (agnetDeploymentRecord, bool) {
|
||||||
agnetMu.RLock()
|
agnetMu.RLock()
|
||||||
record, ok := agnetDeployments[deploymentID]
|
record, ok := agnetDeployments[deploymentID]
|
||||||
@@ -716,6 +746,10 @@ func validateOrchestrationPlan(c *gin.Context, plan agnetOrchestrationPlan) bool
|
|||||||
agnetError(c, "POLICY_REJECTED", "risk_level must be low/medium/high")
|
agnetError(c, "POLICY_REJECTED", "risk_level must be low/medium/high")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if !isValidAgnetSubMode(plan.SubMode) {
|
||||||
|
agnetError(c, "POLICY_REJECTED", "sub_mode must be agile or waterfall")
|
||||||
|
return false
|
||||||
|
}
|
||||||
if plan.Budget.MaxTokens <= 0 || plan.Budget.MaxCostUSD <= 0 || plan.Budget.MaxDurationSec <= 0 {
|
if plan.Budget.MaxTokens <= 0 || plan.Budget.MaxCostUSD <= 0 || plan.Budget.MaxDurationSec <= 0 {
|
||||||
agnetError(c, "POLICY_REJECTED", "budget.max_tokens/max_cost_usd/max_duration_sec must be positive")
|
agnetError(c, "POLICY_REJECTED", "budget.max_tokens/max_cost_usd/max_duration_sec must be positive")
|
||||||
return false
|
return false
|
||||||
@@ -881,22 +915,88 @@ func applyAuthenticatedManagerUserContext(c *gin.Context, plan *agnetOrchestrati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AgnetCreateDeployment(c *gin.Context) {
|
func enforceAuthenticatedAgnetUserContext(c *gin.Context, plan *agnetOrchestrationPlan) bool {
|
||||||
|
if plan == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
authenticatedUserID := c.GetInt("id")
|
||||||
|
if authenticatedUserID <= 0 {
|
||||||
|
agnetError(c, "POLICY_REJECTED", "authenticated user is required")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
userID := strconv.Itoa(authenticatedUserID)
|
||||||
|
if requestedUserID := strings.TrimSpace(plan.UserContext.UserID); requestedUserID != "" && requestedUserID != userID {
|
||||||
|
agnetError(c, "USER_CONTEXT_FORBIDDEN", "user_context.user_id must match authenticated user")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
plan.UserContext.UserID = userID
|
||||||
|
if strings.TrimSpace(plan.UserContext.ChannelID) == "" {
|
||||||
|
if group, ok := c.Get("group"); ok {
|
||||||
|
if channelID, ok := group.(string); ok {
|
||||||
|
plan.UserContext.ChannelID = strings.TrimSpace(channelID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for agentIdx := range plan.Agents {
|
||||||
|
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
|
||||||
|
grantUserID := strings.TrimSpace(plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID)
|
||||||
|
if grantUserID != "" && grantUserID != userID {
|
||||||
|
agnetError(c, "RESOURCE_GRANT_FORBIDDEN", "resource_grants.user_id must match authenticated user")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = userID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func agnetDeploymentBelongsToAuthenticatedUser(c *gin.Context, record agnetDeploymentRecord) bool {
|
||||||
|
userID := strconv.Itoa(c.GetInt("id"))
|
||||||
|
return userID != "0" && strings.TrimSpace(record.Plan.UserContext.UserID) == userID
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireAuthenticatedUserAgnetDeployment(c *gin.Context) (agnetDeploymentRecord, bool) {
|
||||||
|
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
||||||
|
if deploymentID == "" {
|
||||||
|
agnetError(c, "POLICY_REJECTED", "deployment_id is required")
|
||||||
|
return agnetDeploymentRecord{}, false
|
||||||
|
}
|
||||||
|
record, ok := findAgnetDeploymentRecord(deploymentID)
|
||||||
|
if !ok {
|
||||||
|
agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found")
|
||||||
|
return agnetDeploymentRecord{}, false
|
||||||
|
}
|
||||||
|
if !agnetDeploymentBelongsToAuthenticatedUser(c, record) {
|
||||||
|
agnetError(c, "DEPLOYMENT_FORBIDDEN", "deployment does not belong to authenticated user")
|
||||||
|
return agnetDeploymentRecord{}, false
|
||||||
|
}
|
||||||
|
return record, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAgnetDeployment(c *gin.Context, enforceUserScope bool) {
|
||||||
var req agnetDeploymentRequest
|
var req agnetDeploymentRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
agnetError(c, "POLICY_REJECTED", err.Error())
|
agnetError(c, "POLICY_REJECTED", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
plan := req.Plan
|
plan := req.Plan
|
||||||
|
if enforceUserScope {
|
||||||
|
if !enforceAuthenticatedAgnetUserContext(c, &plan) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
applyAuthenticatedManagerUserContext(c, &plan)
|
applyAuthenticatedManagerUserContext(c, &plan)
|
||||||
|
}
|
||||||
if !validateOrchestrationPlan(c, plan) {
|
if !validateOrchestrationPlan(c, plan) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
plan.SubMode = normalizeAgnetSubMode(plan.SubMode)
|
||||||
|
|
||||||
now := agnetNow()
|
now := agnetNow()
|
||||||
deploymentID := "dep_" + common.GetUUID()[:12]
|
deploymentID := "dep_" + common.GetUUID()[:12]
|
||||||
record := agnetDeploymentRecord{
|
record := agnetDeploymentRecord{
|
||||||
DeploymentID: deploymentID,
|
DeploymentID: deploymentID,
|
||||||
|
SubMode: plan.SubMode,
|
||||||
Status: "accepted",
|
Status: "accepted",
|
||||||
Phase: "pending",
|
Phase: "pending",
|
||||||
RuntimeState: "queued",
|
RuntimeState: "queued",
|
||||||
@@ -931,6 +1031,7 @@ func AgnetCreateDeployment(c *gin.Context) {
|
|||||||
|
|
||||||
common.ApiSuccess(c, gin.H{
|
common.ApiSuccess(c, gin.H{
|
||||||
"deployment_id": deploymentID,
|
"deployment_id": deploymentID,
|
||||||
|
"sub_mode": record.SubMode,
|
||||||
"status": record.Status,
|
"status": record.Status,
|
||||||
"phase": record.Phase,
|
"phase": record.Phase,
|
||||||
"runtime_state": record.RuntimeState,
|
"runtime_state": record.RuntimeState,
|
||||||
@@ -940,6 +1041,14 @@ func AgnetCreateDeployment(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetCreateDeployment(c *gin.Context) {
|
||||||
|
createAgnetDeployment(c, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AgnetCreateUserDeployment(c *gin.Context) {
|
||||||
|
createAgnetDeployment(c, true)
|
||||||
|
}
|
||||||
|
|
||||||
func AgnetGetDeployment(c *gin.Context) {
|
func AgnetGetDeployment(c *gin.Context) {
|
||||||
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
||||||
if deploymentID == "" {
|
if deploymentID == "" {
|
||||||
@@ -954,6 +1063,14 @@ func AgnetGetDeployment(c *gin.Context) {
|
|||||||
common.ApiSuccess(c, record)
|
common.ApiSuccess(c, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetGetUserDeployment(c *gin.Context) {
|
||||||
|
record, ok := requireAuthenticatedUserAgnetDeployment(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.ApiSuccess(c, record)
|
||||||
|
}
|
||||||
|
|
||||||
func AgnetListDeployments(c *gin.Context) {
|
func AgnetListDeployments(c *gin.Context) {
|
||||||
userID := strings.TrimSpace(c.Query("user_id"))
|
userID := strings.TrimSpace(c.Query("user_id"))
|
||||||
bindingScope := strings.TrimSpace(c.Query("binding_scope"))
|
bindingScope := strings.TrimSpace(c.Query("binding_scope"))
|
||||||
@@ -965,6 +1082,16 @@ func AgnetListDeployments(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetListUserDeployments(c *gin.Context) {
|
||||||
|
userID := strconv.Itoa(c.GetInt("id"))
|
||||||
|
bindingScope := strings.TrimSpace(c.Query("binding_scope"))
|
||||||
|
items := listAgnetDeploymentRecords(userID, bindingScope)
|
||||||
|
common.ApiSuccess(c, gin.H{
|
||||||
|
"items": items,
|
||||||
|
"total": len(items),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AgnetStopDeployment(c *gin.Context) {
|
func AgnetStopDeployment(c *gin.Context) {
|
||||||
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
||||||
if deploymentID == "" {
|
if deploymentID == "" {
|
||||||
@@ -1016,6 +1143,13 @@ func AgnetStopDeployment(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetStopUserDeployment(c *gin.Context) {
|
||||||
|
if _, ok := requireAuthenticatedUserAgnetDeployment(c); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AgnetStopDeployment(c)
|
||||||
|
}
|
||||||
|
|
||||||
func AgnetListDeploymentEvents(c *gin.Context) {
|
func AgnetListDeploymentEvents(c *gin.Context) {
|
||||||
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
||||||
if deploymentID == "" {
|
if deploymentID == "" {
|
||||||
@@ -1039,6 +1173,90 @@ func AgnetListDeploymentEvents(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetListUserDeploymentEvents(c *gin.Context) {
|
||||||
|
if _, ok := requireAuthenticatedUserAgnetDeployment(c); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AgnetListDeploymentEvents(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAgnetSimulationEvents(values []string) []string {
|
||||||
|
events := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
event := strings.TrimSpace(value)
|
||||||
|
if event == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(event, "simulation.") {
|
||||||
|
event = "simulation." + event
|
||||||
|
}
|
||||||
|
events = append(events, event)
|
||||||
|
}
|
||||||
|
if len(events) == 0 {
|
||||||
|
return []string{
|
||||||
|
"simulation.deployment.started",
|
||||||
|
"simulation.agent.progress",
|
||||||
|
"simulation.deployment.completed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
func AgnetSimulateUserDeploymentEvents(c *gin.Context) {
|
||||||
|
record, ok := requireAuthenticatedUserAgnetDeployment(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req agnetSimulateDeploymentEventsRequest
|
||||||
|
if c.Request != nil && c.Request.Body != nil {
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
agnetError(c, "POLICY_REJECTED", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events := normalizeAgnetSimulationEvents(req.Events)
|
||||||
|
now := agnetNow()
|
||||||
|
record.Phase = "simulated"
|
||||||
|
record.RuntimeState = "simulated"
|
||||||
|
record.FailureReason = ""
|
||||||
|
record.UpdatedAt = now
|
||||||
|
for i := range record.AgentInstances {
|
||||||
|
record.AgentInstances[i].Phase = record.Phase
|
||||||
|
record.AgentInstances[i].RuntimeState = record.RuntimeState
|
||||||
|
record.AgentInstances[i].FailureReason = ""
|
||||||
|
}
|
||||||
|
agnetMu.Lock()
|
||||||
|
agnetDeployments[record.DeploymentID] = record
|
||||||
|
agnetMu.Unlock()
|
||||||
|
if err := updateAgnetDeploymentRecord(record); err != nil {
|
||||||
|
common.SysLog("AgnetSimulateUserDeploymentEvents persist: " + err.Error())
|
||||||
|
agnetError(c, "DEPLOYMENT_PERSIST_FAILED", "failed to persist simulated deployment state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, eventName := range events {
|
||||||
|
recordAgnetAuditEvent(agnetEvent{
|
||||||
|
EventID: "evt_" + common.GetUUID()[:12],
|
||||||
|
Event: eventName,
|
||||||
|
SchemaVersion: 1,
|
||||||
|
UserID: record.Plan.UserContext.UserID,
|
||||||
|
ChannelID: record.Plan.UserContext.ChannelID,
|
||||||
|
BindingScope: firstPlanBindingScope(record.Plan),
|
||||||
|
DeploymentID: record.DeploymentID,
|
||||||
|
CorrelationID: record.Plan.Metadata.CorrelationID,
|
||||||
|
OccurredAt: agnetNow(),
|
||||||
|
}, "agnet_simulator", record.DeploymentID, agnetRequestID(c), "simulated")
|
||||||
|
}
|
||||||
|
|
||||||
|
common.ApiSuccess(c, gin.H{
|
||||||
|
"deployment_id": record.DeploymentID,
|
||||||
|
"simulated": true,
|
||||||
|
"events": events,
|
||||||
|
"total": len(events),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AgnetListDeploymentLogs(c *gin.Context) {
|
func AgnetListDeploymentLogs(c *gin.Context) {
|
||||||
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
||||||
if deploymentID == "" {
|
if deploymentID == "" {
|
||||||
@@ -1097,6 +1315,13 @@ func AgnetListDeploymentLogs(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetListUserDeploymentLogs(c *gin.Context) {
|
||||||
|
if _, ok := requireAuthenticatedUserAgnetDeployment(c); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AgnetListDeploymentLogs(c)
|
||||||
|
}
|
||||||
|
|
||||||
func AgnetGetDeploymentMetrics(c *gin.Context) {
|
func AgnetGetDeploymentMetrics(c *gin.Context) {
|
||||||
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
|
||||||
if deploymentID == "" {
|
if deploymentID == "" {
|
||||||
@@ -1130,6 +1355,13 @@ func AgnetGetDeploymentMetrics(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgnetGetUserDeploymentMetrics(c *gin.Context) {
|
||||||
|
if _, ok := requireAuthenticatedUserAgnetDeployment(c); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AgnetGetDeploymentMetrics(c)
|
||||||
|
}
|
||||||
|
|
||||||
func AgnetProjectDashboardSnapshot(c *gin.Context) {
|
func AgnetProjectDashboardSnapshot(c *gin.Context) {
|
||||||
bindingScope := strings.TrimSpace(c.Param("project_id"))
|
bindingScope := strings.TrimSpace(c.Param("project_id"))
|
||||||
if bindingScope == "" {
|
if bindingScope == "" {
|
||||||
|
|||||||
@@ -177,6 +177,27 @@ func postAgnetCreateDeployment(t *testing.T, plan agnetOrchestrationPlan) (*http
|
|||||||
return recorder, envelope
|
return recorder, envelope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func postAgnetCreateUserDeployment(t *testing.T, userID int, plan agnetOrchestrationPlan) (*httptest.ResponseRecorder, agnetCreateTestEnvelope) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
body, err := common.Marshal(agnetDeploymentRequest{Plan: plan})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set("id", userID)
|
||||||
|
ctx.Set("group", "development")
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments", strings.NewReader(string(body)))
|
||||||
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
AgnetCreateUserDeployment(ctx)
|
||||||
|
|
||||||
|
var envelope agnetCreateTestEnvelope
|
||||||
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
||||||
|
return recorder, envelope
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgnetDeploymentSurvivesInProcessStateReset(t *testing.T) {
|
func TestAgnetDeploymentSurvivesInProcessStateReset(t *testing.T) {
|
||||||
db := setupAgnetControlPlaneTestDB(t)
|
db := setupAgnetControlPlaneTestDB(t)
|
||||||
resetAgnetControlPlaneState(t)
|
resetAgnetControlPlaneState(t)
|
||||||
@@ -228,6 +249,311 @@ func TestAgnetDeploymentSurvivesInProcessStateReset(t *testing.T) {
|
|||||||
require.Equal(t, float64(1), listData["total"])
|
require.Equal(t, float64(1), listData["total"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgnetDeploymentPersistsSubMode(t *testing.T) {
|
||||||
|
db := setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.SubMode = "waterfall"
|
||||||
|
|
||||||
|
recorder, envelope := postAgnetCreateDeployment(t, plan)
|
||||||
|
require.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
require.True(t, envelope.Success)
|
||||||
|
require.Contains(t, recorder.Body.String(), `"sub_mode":"waterfall"`)
|
||||||
|
|
||||||
|
var createBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
||||||
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||||
|
|
||||||
|
var stored model.AgnetDeployment
|
||||||
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
||||||
|
require.Equal(t, "waterfall", stored.SubMode)
|
||||||
|
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
getRecorder := httptest.NewRecorder()
|
||||||
|
getCtx, _ := gin.CreateTestContext(getRecorder)
|
||||||
|
getCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
|
getCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/deployments/"+deploymentID, nil)
|
||||||
|
AgnetGetDeployment(getCtx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, getRecorder.Code)
|
||||||
|
require.Contains(t, getRecorder.Body.String(), `"sub_mode":"waterfall"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetDeploymentDefaultsSubModeToAgile(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.SubMode = ""
|
||||||
|
|
||||||
|
recorder, envelope := postAgnetCreateDeployment(t, plan)
|
||||||
|
require.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
require.True(t, envelope.Success)
|
||||||
|
require.Contains(t, recorder.Body.String(), `"sub_mode":"agile"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetCreateDeploymentRejectsInvalidSubMode(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.SubMode = "scrum"
|
||||||
|
|
||||||
|
_, envelope := postAgnetCreateDeployment(t, plan)
|
||||||
|
require.False(t, envelope.Success)
|
||||||
|
require.Equal(t, "POLICY_REJECTED", envelope.Error.Code)
|
||||||
|
require.Empty(t, agnetDeployments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetUserDeploymentForcesAuthenticatedUserScope(t *testing.T) {
|
||||||
|
db := setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.UserContext.UserID = ""
|
||||||
|
plan.UserContext.ChannelID = ""
|
||||||
|
for agentIdx := range plan.Agents {
|
||||||
|
for grantIdx := range plan.Agents[agentIdx].ResourceGrants {
|
||||||
|
plan.Agents[agentIdx].ResourceGrants[grantIdx].UserID = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder, envelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||||
|
require.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
require.True(t, envelope.Success)
|
||||||
|
|
||||||
|
var createBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &createBody))
|
||||||
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||||
|
|
||||||
|
var stored model.AgnetDeployment
|
||||||
|
require.NoError(t, db.Where("deployment_id = ?", deploymentID).First(&stored).Error)
|
||||||
|
require.Equal(t, "7", stored.UserID)
|
||||||
|
require.Equal(t, "development", stored.ChannelID)
|
||||||
|
require.Contains(t, stored.PlanJSON, `"user_id":"7"`)
|
||||||
|
require.NotContains(t, stored.PlanJSON, `"user_id":"user-p1"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetUserDeploymentRejectsMismatchedUserScope(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.UserContext.UserID = "8"
|
||||||
|
|
||||||
|
_, envelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||||
|
require.False(t, envelope.Success)
|
||||||
|
require.Equal(t, "USER_CONTEXT_FORBIDDEN", envelope.Error.Code)
|
||||||
|
require.Empty(t, agnetDeployments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetUserDeploymentListIsScopedToAuthenticatedUser(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
user7Plan := baseAgnetResourceGrantPlan()
|
||||||
|
user7Plan.UserContext.UserID = "7"
|
||||||
|
user7Plan.Metadata.CorrelationID = "corr-user-7"
|
||||||
|
for idx := range user7Plan.Agents[0].ResourceGrants {
|
||||||
|
user7Plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
||||||
|
}
|
||||||
|
_, user7Envelope := postAgnetCreateDeployment(t, user7Plan)
|
||||||
|
require.True(t, user7Envelope.Success)
|
||||||
|
|
||||||
|
user8Plan := baseAgnetResourceGrantPlan()
|
||||||
|
user8Plan.UserContext.UserID = "8"
|
||||||
|
user8Plan.Metadata.CorrelationID = "corr-user-8"
|
||||||
|
for idx := range user8Plan.Agents[0].ResourceGrants {
|
||||||
|
user8Plan.Agents[0].ResourceGrants[idx].UserID = "8"
|
||||||
|
}
|
||||||
|
_, user8Envelope := postAgnetCreateDeployment(t, user8Plan)
|
||||||
|
require.True(t, user8Envelope.Success)
|
||||||
|
|
||||||
|
listRecorder := httptest.NewRecorder()
|
||||||
|
listCtx, _ := gin.CreateTestContext(listRecorder)
|
||||||
|
listCtx.Set("id", 7)
|
||||||
|
listCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments", nil)
|
||||||
|
AgnetListUserDeployments(listCtx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, listRecorder.Code)
|
||||||
|
var listBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(listRecorder.Body.Bytes(), &listBody))
|
||||||
|
require.Equal(t, true, listBody["success"])
|
||||||
|
listData := listBody["data"].(map[string]any)
|
||||||
|
require.Equal(t, float64(1), listData["total"])
|
||||||
|
items := listData["items"].([]any)
|
||||||
|
item := items[0].(map[string]any)
|
||||||
|
planData := item["orchestration_plan"].(map[string]any)
|
||||||
|
userContext := planData["user_context"].(map[string]any)
|
||||||
|
require.Equal(t, "7", userContext["user_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetTaskDeploymentDraftBuildsSafePlan(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
body := `{
|
||||||
|
"task":{"id":"task_bridge_1","name":"Ship Manager sub mode","intent":"Need a scoped implementation","status":"running","card":{"goal":"Implement the Manager-side sub task flow"}},
|
||||||
|
"sub_mode":"waterfall",
|
||||||
|
"binding_scope":"repo-main",
|
||||||
|
"role_templates":["backend"],
|
||||||
|
"default_model_id":"agnet-model-builder",
|
||||||
|
"budget":{"max_tokens":10000,"max_cost_usd":2,"max_duration_sec":900}
|
||||||
|
}`
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set("id", 7)
|
||||||
|
ctx.Set("group", "development")
|
||||||
|
ctx.Params = gin.Params{{Key: "task_id", Value: "task_bridge_1"}}
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/tasks/task_bridge_1/deployment-draft", strings.NewReader(body))
|
||||||
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
AgnetCreateTaskDeploymentDraft(ctx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
require.NotContains(t, strings.ToLower(recorder.Body.String()), "password")
|
||||||
|
require.NotContains(t, strings.ToLower(recorder.Body.String()), "access_token")
|
||||||
|
var env map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &env))
|
||||||
|
require.Equal(t, true, env["success"])
|
||||||
|
data := env["data"].(map[string]any)
|
||||||
|
plan := data["orchestration_plan"].(map[string]any)
|
||||||
|
require.Equal(t, "task_bridge_1", plan["intent_id"])
|
||||||
|
require.Equal(t, "waterfall", plan["sub_mode"])
|
||||||
|
require.Equal(t, "Implement the Manager-side sub task flow", plan["objective"])
|
||||||
|
userContext := plan["user_context"].(map[string]any)
|
||||||
|
require.Equal(t, "7", userContext["user_id"])
|
||||||
|
agents := plan["agents"].([]any)
|
||||||
|
agent := agents[0].(map[string]any)
|
||||||
|
grants := agent["resource_grants"].([]any)
|
||||||
|
grant := grants[0].(map[string]any)
|
||||||
|
require.Equal(t, "project_doc", grant["resource_type"])
|
||||||
|
require.Equal(t, "7", grant["user_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetTaskDeploymentDraftRequiresTaskSnapshot(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set("id", 7)
|
||||||
|
ctx.Params = gin.Params{{Key: "task_id", Value: "task_missing"}}
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/tasks/task_missing/deployment-draft", strings.NewReader(`{"sub_mode":"agile"}`))
|
||||||
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
AgnetCreateTaskDeploymentDraft(ctx)
|
||||||
|
|
||||||
|
var envelope agnetCreateTestEnvelope
|
||||||
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
||||||
|
require.False(t, envelope.Success)
|
||||||
|
require.Equal(t, "TASK_NOT_FOUND", envelope.Error.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetTaskDeploymentDraftRejectsCredentialGrantWithoutSecretRef(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
body := `{
|
||||||
|
"task":{"id":"task_bridge_git","name":"Use a git repo","intent":"Need repo access","status":"running"},
|
||||||
|
"sub_mode":"agile",
|
||||||
|
"binding_scope":"repo-main",
|
||||||
|
"role_templates":["backend"],
|
||||||
|
"default_model_id":"agnet-model-builder",
|
||||||
|
"resource_grants":[{
|
||||||
|
"resource_id":"repo-1",
|
||||||
|
"resource_type":"git",
|
||||||
|
"permission_scope":["repo:read"]
|
||||||
|
}]
|
||||||
|
}`
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set("id", 7)
|
||||||
|
ctx.Params = gin.Params{{Key: "task_id", Value: "task_bridge_git"}}
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/tasks/task_bridge_git/deployment-draft", strings.NewReader(body))
|
||||||
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
AgnetCreateTaskDeploymentDraft(ctx)
|
||||||
|
|
||||||
|
var envelope agnetCreateTestEnvelope
|
||||||
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &envelope))
|
||||||
|
require.False(t, envelope.Success)
|
||||||
|
require.Equal(t, "RESOURCE_GRANT_SECRET_REF_REQUIRED", envelope.Error.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetUserDeploymentSimulatedEventsArePersistedAndPrefixed(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.UserContext.UserID = "7"
|
||||||
|
for idx := range plan.Agents[0].ResourceGrants {
|
||||||
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
||||||
|
}
|
||||||
|
createRecorder, createEnvelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||||
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
||||||
|
require.True(t, createEnvelope.Success)
|
||||||
|
var createBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
||||||
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||||
|
|
||||||
|
simRecorder := httptest.NewRecorder()
|
||||||
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
||||||
|
simCtx.Set("id", 7)
|
||||||
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{"events":["deployment.started","simulation.agent.done"]}`))
|
||||||
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
AgnetSimulateUserDeploymentEvents(simCtx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, simRecorder.Code)
|
||||||
|
require.Contains(t, simRecorder.Body.String(), `"simulated":true`)
|
||||||
|
require.Contains(t, simRecorder.Body.String(), `"simulation.deployment.started"`)
|
||||||
|
require.Contains(t, simRecorder.Body.String(), `"simulation.agent.done"`)
|
||||||
|
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
eventsRecorder := httptest.NewRecorder()
|
||||||
|
eventsCtx, _ := gin.CreateTestContext(eventsRecorder)
|
||||||
|
eventsCtx.Set("id", 7)
|
||||||
|
eventsCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
|
eventsCtx.Request = httptest.NewRequest(http.MethodGet, "/api/agnet/user/deployments/"+deploymentID+"/events", nil)
|
||||||
|
AgnetListUserDeploymentEvents(eventsCtx)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, eventsRecorder.Code)
|
||||||
|
require.Contains(t, eventsRecorder.Body.String(), `"simulation.deployment.started"`)
|
||||||
|
require.Contains(t, eventsRecorder.Body.String(), `"simulation.agent.done"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgnetUserDeploymentSimulationRejectsOtherUsersDeployment(t *testing.T) {
|
||||||
|
setupAgnetControlPlaneTestDB(t)
|
||||||
|
resetAgnetControlPlaneState(t)
|
||||||
|
|
||||||
|
plan := baseAgnetResourceGrantPlan()
|
||||||
|
plan.UserContext.UserID = "7"
|
||||||
|
for idx := range plan.Agents[0].ResourceGrants {
|
||||||
|
plan.Agents[0].ResourceGrants[idx].UserID = "7"
|
||||||
|
}
|
||||||
|
createRecorder, createEnvelope := postAgnetCreateUserDeployment(t, 7, plan)
|
||||||
|
require.Equal(t, http.StatusOK, createRecorder.Code)
|
||||||
|
require.True(t, createEnvelope.Success)
|
||||||
|
var createBody map[string]any
|
||||||
|
require.NoError(t, common.Unmarshal(createRecorder.Body.Bytes(), &createBody))
|
||||||
|
deploymentID := createBody["data"].(map[string]any)["deployment_id"].(string)
|
||||||
|
|
||||||
|
simRecorder := httptest.NewRecorder()
|
||||||
|
simCtx, _ := gin.CreateTestContext(simRecorder)
|
||||||
|
simCtx.Set("id", 8)
|
||||||
|
simCtx.Params = gin.Params{{Key: "deployment_id", Value: deploymentID}}
|
||||||
|
simCtx.Request = httptest.NewRequest(http.MethodPost, "/api/agnet/user/deployments/"+deploymentID+"/simulate-events", strings.NewReader(`{}`))
|
||||||
|
simCtx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
AgnetSimulateUserDeploymentEvents(simCtx)
|
||||||
|
|
||||||
|
var envelope agnetCreateTestEnvelope
|
||||||
|
require.NoError(t, common.Unmarshal(simRecorder.Body.Bytes(), &envelope))
|
||||||
|
require.False(t, envelope.Success)
|
||||||
|
require.Equal(t, "DEPLOYMENT_FORBIDDEN", envelope.Error.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgnetStopDeploymentPersistsState(t *testing.T) {
|
func TestAgnetStopDeploymentPersistsState(t *testing.T) {
|
||||||
setupAgnetControlPlaneTestDB(t)
|
setupAgnetControlPlaneTestDB(t)
|
||||||
resetAgnetControlPlaneState(t)
|
resetAgnetControlPlaneState(t)
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
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 = "agnet-model-" + sanitizeAgnetRef(role)
|
||||||
|
}
|
||||||
|
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 = "agnet-model-" + sanitizeAgnetRef(role)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ type AgnetDeployment struct {
|
|||||||
ChannelID string `gorm:"type:varchar(64)" json:"channel_id"`
|
ChannelID string `gorm:"type:varchar(64)" json:"channel_id"`
|
||||||
BindingScope string `gorm:"type:varchar(512);index" json:"binding_scope"`
|
BindingScope string `gorm:"type:varchar(512);index" json:"binding_scope"`
|
||||||
CorrelationID string `gorm:"type:varchar(64);index" json:"correlation_id"`
|
CorrelationID string `gorm:"type:varchar(64);index" json:"correlation_id"`
|
||||||
|
SubMode string `gorm:"type:varchar(32);index" json:"sub_mode"`
|
||||||
Status string `gorm:"type:varchar(32);index" json:"status"`
|
Status string `gorm:"type:varchar(32);index" json:"status"`
|
||||||
Phase string `gorm:"type:varchar(32);index" json:"phase"`
|
Phase string `gorm:"type:varchar(32);index" json:"phase"`
|
||||||
RuntimeState string `gorm:"type:varchar(32)" json:"runtime_state"`
|
RuntimeState string `gorm:"type:varchar(32)" json:"runtime_state"`
|
||||||
|
|||||||
@@ -499,6 +499,16 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
agnetApprovalRoute.POST("/approvals/:approval_id/reject", controller.RejectAgnetApprovalRequest)
|
agnetApprovalRoute.POST("/approvals/:approval_id/reject", controller.RejectAgnetApprovalRequest)
|
||||||
agnetApprovalRoute.GET("/credential-leases", controller.ListAgnetCredentialLeases)
|
agnetApprovalRoute.GET("/credential-leases", controller.ListAgnetCredentialLeases)
|
||||||
agnetApprovalRoute.POST("/credential-leases/:lease_id/revoke", controller.RevokeAgnetCredentialLease)
|
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/events", controller.AgnetListUserDeploymentEvents)
|
||||||
|
agnetApprovalRoute.POST("/user/deployments/:deployment_id/simulate-events", controller.AgnetSimulateUserDeploymentEvents)
|
||||||
|
agnetApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgnetCreateTaskDeploymentDraft)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agnet orchestration control plane (minimal integration endpoints)
|
// Agnet orchestration control plane (minimal integration endpoints)
|
||||||
@@ -516,12 +526,6 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
agnetRoute.POST("/sk-snapshots/resolve", controller.AgnetResolveSKSnapshots)
|
agnetRoute.POST("/sk-snapshots/resolve", controller.AgnetResolveSKSnapshots)
|
||||||
agnetRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgnetProjectDashboardSnapshot)
|
agnetRoute.GET("/projects/:project_id/dashboard-snapshot", controller.AgnetProjectDashboardSnapshot)
|
||||||
agnetRoute.GET("/audit-logs", controller.AgnetListAuditLogs)
|
agnetRoute.GET("/audit-logs", controller.AgnetListAuditLogs)
|
||||||
// Platform-recommended role catalog. Six canonical Agnet
|
|
||||||
// roles per docs/product-package/13-platform-description.md
|
|
||||||
// §3 (Product / Architect / Frontend / Backend / Reviewer /
|
|
||||||
// Ops). UI uses this to populate the deployment-creation
|
|
||||||
// role picker. Read-only, no secrets in payload.
|
|
||||||
agnetRoute.GET("/role-templates", controller.AgnetListRoleTemplates)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-4
@@ -1,5 +1,7 @@
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
|
|
||||||
|
export type AgnetSubMode = 'agile' | 'waterfall'
|
||||||
|
|
||||||
/** Sub-agent cloud/runtime binding (passed to Agnet on deploy). */
|
/** Sub-agent cloud/runtime binding (passed to Agnet on deploy). */
|
||||||
export type AgnetRuntimeExecution = {
|
export type AgnetRuntimeExecution = {
|
||||||
profile_id?: string
|
profile_id?: string
|
||||||
@@ -107,6 +109,7 @@ export type AgnetOrchestrationPlan = {
|
|||||||
intent_id: string
|
intent_id: string
|
||||||
template_hint: string
|
template_hint: string
|
||||||
objective: string
|
objective: string
|
||||||
|
sub_mode?: AgnetSubMode
|
||||||
risk_level: 'low' | 'medium' | 'high'
|
risk_level: 'low' | 'medium' | 'high'
|
||||||
budget: AgnetBudget
|
budget: AgnetBudget
|
||||||
user_context: AgnetUserContext
|
user_context: AgnetUserContext
|
||||||
@@ -123,6 +126,7 @@ export type AgnetCreateDeploymentBody = {
|
|||||||
|
|
||||||
export type AgnetCreateDeploymentResult = {
|
export type AgnetCreateDeploymentResult = {
|
||||||
deployment_id: string
|
deployment_id: string
|
||||||
|
sub_mode?: AgnetSubMode
|
||||||
status: string
|
status: string
|
||||||
agent_instances?: Array<{
|
agent_instances?: Array<{
|
||||||
instance_id?: string
|
instance_id?: string
|
||||||
@@ -151,6 +155,7 @@ export type AgnetPermissionManifest = {
|
|||||||
|
|
||||||
export type AgnetDeployment = {
|
export type AgnetDeployment = {
|
||||||
deployment_id: string
|
deployment_id: string
|
||||||
|
sub_mode?: AgnetSubMode
|
||||||
status: string
|
status: string
|
||||||
phase: string
|
phase: string
|
||||||
runtime_state?: string
|
runtime_state?: string
|
||||||
@@ -162,6 +167,7 @@ export type AgnetDeployment = {
|
|||||||
intent_id?: string
|
intent_id?: string
|
||||||
template_hint?: string
|
template_hint?: string
|
||||||
objective?: string
|
objective?: string
|
||||||
|
sub_mode?: AgnetSubMode
|
||||||
risk_level?: string
|
risk_level?: string
|
||||||
budget?: AgnetBudget
|
budget?: AgnetBudget
|
||||||
agents?: AgnetAgentPlan[]
|
agents?: AgnetAgentPlan[]
|
||||||
@@ -276,14 +282,14 @@ export async function listAgnetRoleTemplates(): Promise<AgnetRoleTemplate[]> {
|
|||||||
|
|
||||||
export async function listAgnetDeployments(): Promise<AgnetDeployment[]> {
|
export async function listAgnetDeployments(): Promise<AgnetDeployment[]> {
|
||||||
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
|
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
|
||||||
'/api/agnet/deployments'
|
'/api/agnet/user/deployments'
|
||||||
)
|
)
|
||||||
return res.data?.data?.items ?? []
|
return res.data?.data?.items ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAgnetDeploymentsQuiet(): Promise<AgnetDeployment[]> {
|
export async function listAgnetDeploymentsQuiet(): Promise<AgnetDeployment[]> {
|
||||||
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
|
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
|
||||||
'/api/agnet/deployments',
|
'/api/agnet/user/deployments',
|
||||||
{
|
{
|
||||||
skipBusinessError: true,
|
skipBusinessError: true,
|
||||||
skipErrorHandler: true,
|
skipErrorHandler: true,
|
||||||
@@ -297,7 +303,7 @@ export async function createAgnetDeployment(
|
|||||||
body: AgnetCreateDeploymentBody
|
body: AgnetCreateDeploymentBody
|
||||||
): Promise<AgnetCreateDeploymentResult> {
|
): Promise<AgnetCreateDeploymentResult> {
|
||||||
const res = await api.post<ApiEnvelope<AgnetCreateDeploymentResult>>(
|
const res = await api.post<ApiEnvelope<AgnetCreateDeploymentResult>>(
|
||||||
'/api/agnet/deployments',
|
'/api/agnet/user/deployments',
|
||||||
body
|
body
|
||||||
)
|
)
|
||||||
const env = res.data
|
const env = res.data
|
||||||
@@ -314,10 +320,26 @@ export async function createAgnetDeployment(
|
|||||||
export async function getAgnetDeploymentEvents(deploymentId: string) {
|
export async function getAgnetDeploymentEvents(deploymentId: string) {
|
||||||
const res = await api.get<
|
const res = await api.get<
|
||||||
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
|
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
|
||||||
>(`/api/agnet/deployments/${deploymentId}/events`)
|
>(`/api/agnet/user/deployments/${deploymentId}/events`)
|
||||||
return res.data?.data?.items ?? []
|
return res.data?.data?.items ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function simulateAgnetDeploymentEvents(
|
||||||
|
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`, {
|
||||||
|
events: events ?? [],
|
||||||
|
})
|
||||||
|
const env = res.data
|
||||||
|
if (!env?.success || !env.data) {
|
||||||
|
throw new Error(env?.message || 'simulateAgnetDeploymentEvents failed')
|
||||||
|
}
|
||||||
|
return env.data
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAgnetAuditLogs() {
|
export async function getAgnetAuditLogs() {
|
||||||
const res = await api.get<
|
const res = await api.get<
|
||||||
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
|
ApiEnvelope<{ items?: Array<Record<string, unknown>> }>
|
||||||
|
|||||||
+86
-50
@@ -1,7 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState, type ComponentType } from 'react'
|
import { useEffect, useMemo, useState, type ComponentType } from 'react'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -15,6 +13,10 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { useAuthStore } from '@/stores/auth-store'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
@@ -37,8 +39,6 @@ import {
|
|||||||
} from '@/components/ui/sheet'
|
} from '@/components/ui/sheet'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { useAuthStore } from '@/stores/auth-store'
|
|
||||||
import {
|
import {
|
||||||
createAgnetDeployment,
|
createAgnetDeployment,
|
||||||
listAgnetRoleTemplates,
|
listAgnetRoleTemplates,
|
||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
type AgnetOrchestrationPlan,
|
type AgnetOrchestrationPlan,
|
||||||
type AgnetRoleTemplate,
|
type AgnetRoleTemplate,
|
||||||
type AgnetSKSource,
|
type AgnetSKSource,
|
||||||
|
type AgnetSubMode,
|
||||||
} from './api'
|
} from './api'
|
||||||
|
|
||||||
type ResourceType =
|
type ResourceType =
|
||||||
@@ -82,7 +83,9 @@ type TemplatePreset = {
|
|||||||
label: string
|
label: string
|
||||||
objective: string
|
objective: string
|
||||||
risk: 'low' | 'medium' | 'high'
|
risk: 'low' | 'medium' | 'high'
|
||||||
roles: Array<Pick<AgentFormRow, 'role_template' | 'goal' | 'default_model_id'>>
|
roles: Array<
|
||||||
|
Pick<AgentFormRow, 'role_template' | 'goal' | 'default_model_id'>
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
const RESOURCE_TYPES: ResourceType[] = [
|
const RESOURCE_TYPES: ResourceType[] = [
|
||||||
@@ -338,10 +341,10 @@ function SectionTitle({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-start gap-2'>
|
<div className='flex items-start gap-2'>
|
||||||
<Icon className='mt-0.5 h-4 w-4 text-primary' />
|
<Icon className='text-primary mt-0.5 h-4 w-4' />
|
||||||
<div>
|
<div>
|
||||||
<p className='text-sm font-semibold'>{title}</p>
|
<p className='text-sm font-semibold'>{title}</p>
|
||||||
{hint ? <p className='text-xs text-muted-foreground'>{hint}</p> : null}
|
{hint ? <p className='text-muted-foreground text-xs'>{hint}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -362,6 +365,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
const [step, setStep] = useState('idea')
|
const [step, setStep] = useState('idea')
|
||||||
const [templateHint, setTemplateHint] = useState(defaultTemplate.id)
|
const [templateHint, setTemplateHint] = useState(defaultTemplate.id)
|
||||||
const [objective, setObjective] = useState(defaultTemplate.objective)
|
const [objective, setObjective] = useState(defaultTemplate.objective)
|
||||||
|
const [subMode, setSubMode] = useState<AgnetSubMode>('agile')
|
||||||
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high'>(
|
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high'>(
|
||||||
defaultTemplate.risk
|
defaultTemplate.risk
|
||||||
)
|
)
|
||||||
@@ -410,6 +414,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
setStep('idea')
|
setStep('idea')
|
||||||
setTemplateHint(defaultTemplate.id)
|
setTemplateHint(defaultTemplate.id)
|
||||||
setObjective(defaultTemplate.objective)
|
setObjective(defaultTemplate.objective)
|
||||||
|
setSubMode('agile')
|
||||||
setRiskLevel(defaultTemplate.risk)
|
setRiskLevel(defaultTemplate.risk)
|
||||||
setMaxTokens(120_000)
|
setMaxTokens(120_000)
|
||||||
setMaxCost(8)
|
setMaxCost(8)
|
||||||
@@ -493,6 +498,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
intent_id: '<generated on submit>',
|
intent_id: '<generated on submit>',
|
||||||
template_hint: templateHint.trim(),
|
template_hint: templateHint.trim(),
|
||||||
objective: objective.trim(),
|
objective: objective.trim(),
|
||||||
|
sub_mode: subMode,
|
||||||
risk_level: riskLevel,
|
risk_level: riskLevel,
|
||||||
resource_scope_ref: resourceScopeRef.trim(),
|
resource_scope_ref: resourceScopeRef.trim(),
|
||||||
agent_runtime: {
|
agent_runtime: {
|
||||||
@@ -522,6 +528,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
objective,
|
objective,
|
||||||
resourceScopeRef,
|
resourceScopeRef,
|
||||||
riskLevel,
|
riskLevel,
|
||||||
|
subMode,
|
||||||
templateHint,
|
templateHint,
|
||||||
userId,
|
userId,
|
||||||
])
|
])
|
||||||
@@ -591,6 +598,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
intent_id: intentId,
|
intent_id: intentId,
|
||||||
template_hint: templateHint.trim(),
|
template_hint: templateHint.trim(),
|
||||||
objective: objective.trim(),
|
objective: objective.trim(),
|
||||||
|
sub_mode: subMode,
|
||||||
risk_level: riskLevel,
|
risk_level: riskLevel,
|
||||||
budget: {
|
budget: {
|
||||||
max_tokens: maxTokens,
|
max_tokens: maxTokens,
|
||||||
@@ -727,10 +735,10 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
<div className='flex items-center justify-between gap-2'>
|
<div className='flex items-center justify-between gap-2'>
|
||||||
<p className='text-sm font-medium'>{template.label}</p>
|
<p className='text-sm font-medium'>{template.label}</p>
|
||||||
{templateHint === template.id ? (
|
{templateHint === template.id ? (
|
||||||
<CheckCircle2 className='h-4 w-4 text-primary' />
|
<CheckCircle2 className='text-primary h-4 w-4' />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<p className='mt-2 line-clamp-3 text-xs text-muted-foreground'>
|
<p className='text-muted-foreground mt-2 line-clamp-3 text-xs'>
|
||||||
{template.objective}
|
{template.objective}
|
||||||
</p>
|
</p>
|
||||||
<Badge variant='outline' className='mt-3 text-[10px]'>
|
<Badge variant='outline' className='mt-3 text-[10px]'>
|
||||||
@@ -751,7 +759,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
placeholder={t('Describe what this work should achieve.')}
|
placeholder={t('Describe what this work should achieve.')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid gap-3 sm:grid-cols-2'>
|
<div className='grid gap-3 sm:grid-cols-3'>
|
||||||
<div className='grid gap-2'>
|
<div className='grid gap-2'>
|
||||||
<Label>{t('Template hint')}</Label>
|
<Label>{t('Template hint')}</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -778,6 +786,21 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<Label>{t('Sub mode')}</Label>
|
||||||
|
<Select
|
||||||
|
value={subMode}
|
||||||
|
onValueChange={(v) => setSubMode(v as AgnetSubMode)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className='h-9'>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='agile'>agile</SelectItem>
|
||||||
|
<SelectItem value='waterfall'>waterfall</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -905,7 +928,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className='h-4 w-4 text-destructive' />
|
<Trash2 className='text-destructive h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -979,8 +1002,8 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
className='text-sm'
|
className='text-sm'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='grid gap-2 rounded-md bg-muted/35 p-3'>
|
<div className='bg-muted/35 grid gap-2 rounded-md p-3'>
|
||||||
<div className='flex items-center gap-2 text-xs font-semibold text-muted-foreground'>
|
<div className='text-muted-foreground flex items-center gap-2 text-xs font-semibold'>
|
||||||
<GitBranch className='h-3.5 w-3.5' />
|
<GitBranch className='h-3.5 w-3.5' />
|
||||||
{t('Resource grant')}
|
{t('Resource grant')}
|
||||||
</div>
|
</div>
|
||||||
@@ -1099,8 +1122,8 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid gap-2 rounded-md bg-muted/35 p-3'>
|
<div className='bg-muted/35 grid gap-2 rounded-md p-3'>
|
||||||
<div className='flex items-center gap-2 text-xs font-semibold text-muted-foreground'>
|
<div className='text-muted-foreground flex items-center gap-2 text-xs font-semibold'>
|
||||||
<KeyRound className='h-3.5 w-3.5' />
|
<KeyRound className='h-3.5 w-3.5' />
|
||||||
{t('Runtime binding')}
|
{t('Runtime binding')}
|
||||||
</div>
|
</div>
|
||||||
@@ -1128,9 +1151,9 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid gap-2 rounded-md bg-muted/35 p-3'>
|
<div className='bg-muted/35 grid gap-2 rounded-md p-3'>
|
||||||
<div className='flex items-center justify-between gap-2'>
|
<div className='flex items-center justify-between gap-2'>
|
||||||
<div className='flex items-center gap-2 text-xs font-semibold text-muted-foreground'>
|
<div className='text-muted-foreground flex items-center gap-2 text-xs font-semibold'>
|
||||||
<FileText className='h-3.5 w-3.5' />
|
<FileText className='h-3.5 w-3.5' />
|
||||||
{t('SK access policy')}
|
{t('SK access policy')}
|
||||||
</div>
|
</div>
|
||||||
@@ -1209,14 +1232,16 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
always matches what gets submitted. */}
|
always matches what gets submitted. */}
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
{/* 1. 本次会做 — Objective + role list */}
|
{/* 1. 本次会做 — Objective + role list */}
|
||||||
<div className='rounded-lg border bg-card/40 p-3'>
|
<div className='bg-card/40 rounded-lg border p-3'>
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.12em] uppercase'>
|
||||||
{t('This run will do')}
|
{t('This run will do')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-2 text-sm text-foreground'>
|
<p className='text-foreground mt-2 text-sm'>
|
||||||
{objective.trim() || (
|
{objective.trim() || (
|
||||||
<span className='text-muted-foreground italic'>
|
<span className='text-muted-foreground italic'>
|
||||||
{t('No objective provided yet — go back to the Idea tab.')}
|
{t(
|
||||||
|
'No objective provided yet — go back to the Idea tab.'
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@@ -1225,7 +1250,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
{previewPlan.agent_runtime.agents.map((a) => (
|
{previewPlan.agent_runtime.agents.map((a) => (
|
||||||
<span
|
<span
|
||||||
key={a.role}
|
key={a.role}
|
||||||
className='inline-flex items-center gap-1 rounded-md bg-[color-mix(in_oklch,var(--primary)_12%,transparent)] px-1.5 py-0.5 font-mono text-[10px] text-primary'
|
className='text-primary inline-flex items-center gap-1 rounded-md bg-[color-mix(in_oklch,var(--primary)_12%,transparent)] px-1.5 py-0.5 font-mono text-[10px]'
|
||||||
>
|
>
|
||||||
{a.role}
|
{a.role}
|
||||||
<span className='text-muted-foreground'>·</span>
|
<span className='text-muted-foreground'>·</span>
|
||||||
@@ -1237,13 +1262,16 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2. 本次允许使用 — Resources from permission_manifest */}
|
{/* 2. 本次允许使用 — Resources from permission_manifest */}
|
||||||
<div className='rounded-lg border bg-card/40 p-3'>
|
<div className='bg-card/40 rounded-lg border p-3'>
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.12em] uppercase'>
|
||||||
{t('Resources this run may use')}
|
{t('Resources this run may use')}
|
||||||
</p>
|
</p>
|
||||||
{previewPlan.permission_manifest.resource_grants.length === 0 ? (
|
{previewPlan.permission_manifest.resource_grants.length ===
|
||||||
<p className='mt-2 text-sm text-muted-foreground italic'>
|
0 ? (
|
||||||
{t('No resources bound yet — Agnet will run with no external data access.')}
|
<p className='text-muted-foreground mt-2 text-sm italic'>
|
||||||
|
{t(
|
||||||
|
'No resources bound yet — Agnet will run with no external data access.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className='mt-2 space-y-1 text-xs'>
|
<ul className='mt-2 space-y-1 text-xs'>
|
||||||
@@ -1256,12 +1284,13 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
}
|
}
|
||||||
className='flex items-center gap-2'
|
className='flex items-center gap-2'
|
||||||
>
|
>
|
||||||
<span className='inline-flex items-center rounded bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-1 font-mono text-[10px] text-primary'>
|
<span className='text-primary inline-flex items-center rounded bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-1 font-mono text-[10px]'>
|
||||||
{(g as { resource_type?: string }).resource_type ||
|
{(g as { resource_type?: string })
|
||||||
'?'}
|
.resource_type || '?'}
|
||||||
</span>
|
</span>
|
||||||
<span className='font-mono text-[11px] text-muted-foreground'>
|
<span className='text-muted-foreground font-mono text-[11px]'>
|
||||||
{(g as { resource_id?: string }).resource_id || '—'}
|
{(g as { resource_id?: string }).resource_id ||
|
||||||
|
'—'}
|
||||||
</span>
|
</span>
|
||||||
<span className='text-muted-foreground'>·</span>
|
<span className='text-muted-foreground'>·</span>
|
||||||
<span className='text-[11px]'>
|
<span className='text-[11px]'>
|
||||||
@@ -1269,10 +1298,14 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
(g as { permission_scope?: string[] })
|
(g as { permission_scope?: string[] })
|
||||||
.permission_scope
|
.permission_scope
|
||||||
) &&
|
) &&
|
||||||
((g as { permission_scope?: string[] })
|
(
|
||||||
.permission_scope as string[]).length > 0
|
(g as { permission_scope?: string[] })
|
||||||
? ((g as { permission_scope?: string[] })
|
.permission_scope as string[]
|
||||||
.permission_scope as string[]).join(', ')
|
).length > 0
|
||||||
|
? (
|
||||||
|
(g as { permission_scope?: string[] })
|
||||||
|
.permission_scope as string[]
|
||||||
|
).join(', ')
|
||||||
: t('no actions specified')}
|
: t('no actions specified')}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -1284,10 +1317,10 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
|
|
||||||
{/* 3. 本次不会做 — Static off-limits list per §10 */}
|
{/* 3. 本次不会做 — Static off-limits list per §10 */}
|
||||||
<div className='rounded-lg border border-dashed border-rose-500/30 bg-rose-500/5 p-3'>
|
<div className='rounded-lg border border-dashed border-rose-500/30 bg-rose-500/5 p-3'>
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-rose-400'>
|
<p className='text-[11px] font-semibold tracking-[0.12em] text-rose-400 uppercase'>
|
||||||
{t('This run will NOT do')}
|
{t('This run will NOT do')}
|
||||||
</p>
|
</p>
|
||||||
<ul className='mt-2 grid gap-1 text-xs text-muted-foreground sm:grid-cols-2'>
|
<ul className='text-muted-foreground mt-2 grid gap-1 text-xs sm:grid-cols-2'>
|
||||||
<li>· {t('Production deploys without client approval')}</li>
|
<li>· {t('Production deploys without client approval')}</li>
|
||||||
<li>· {t('Production database writes')}</li>
|
<li>· {t('Production database writes')}</li>
|
||||||
<li>· {t('Long-lived credential extraction')}</li>
|
<li>· {t('Long-lived credential extraction')}</li>
|
||||||
@@ -1296,11 +1329,11 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4. 高危规则 — Risk level + escalation policy */}
|
{/* 4. 高危规则 — Risk level + escalation policy */}
|
||||||
<div className='rounded-lg border bg-card/40 p-3'>
|
<div className='bg-card/40 rounded-lg border p-3'>
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.12em] uppercase'>
|
||||||
{t('High-risk operations')}
|
{t('High-risk operations')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-2 text-xs text-foreground'>
|
<p className='text-foreground mt-2 text-xs'>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ring-1 ring-inset',
|
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ring-1 ring-inset',
|
||||||
@@ -1313,8 +1346,11 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
>
|
>
|
||||||
{t('Risk level')}: {riskLevel}
|
{t('Risk level')}: {riskLevel}
|
||||||
</span>
|
</span>
|
||||||
|
<span className='text-primary ring-primary/20 ml-2 inline-flex items-center rounded-full bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-2 py-0.5 font-mono text-[10px] ring-1 ring-inset'>
|
||||||
|
sub_mode: {subMode}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-2 text-xs leading-relaxed text-muted-foreground'>
|
<p className='text-muted-foreground mt-2 text-xs leading-relaxed'>
|
||||||
{t(
|
{t(
|
||||||
'Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.'
|
'Any high-risk action (production deploy, secret access, destructive change) requires explicit approval from the desktop client. Short-lived, scoped credentials are issued only at approval time and recorded in audit.'
|
||||||
)}
|
)}
|
||||||
@@ -1322,20 +1358,20 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5. 预计消耗 — Budget triple */}
|
{/* 5. 预计消耗 — Budget triple */}
|
||||||
<div className='rounded-lg border bg-card/40 p-3'>
|
<div className='bg-card/40 rounded-lg border p-3'>
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.12em] uppercase'>
|
||||||
{t('Expected consumption')}
|
{t('Expected consumption')}
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-2 grid gap-2 text-xs sm:grid-cols-3'>
|
<div className='mt-2 grid gap-2 text-xs sm:grid-cols-3'>
|
||||||
<div>
|
<div>
|
||||||
<p className='text-muted-foreground'>{t('Max tokens')}</p>
|
<p className='text-muted-foreground'>{t('Max tokens')}</p>
|
||||||
<p className='font-mono text-sm text-foreground'>
|
<p className='text-foreground font-mono text-sm'>
|
||||||
{maxTokens.toLocaleString()}
|
{maxTokens.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className='text-muted-foreground'>{t('Max cost')}</p>
|
<p className='text-muted-foreground'>{t('Max cost')}</p>
|
||||||
<p className='font-mono text-sm text-foreground'>
|
<p className='text-foreground font-mono text-sm'>
|
||||||
${maxCost}
|
${maxCost}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1343,7 +1379,7 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
<p className='text-muted-foreground'>
|
<p className='text-muted-foreground'>
|
||||||
{t('Max duration')}
|
{t('Max duration')}
|
||||||
</p>
|
</p>
|
||||||
<p className='font-mono text-sm text-foreground'>
|
<p className='text-foreground font-mono text-sm'>
|
||||||
{Math.round(maxDurationSec / 60)} {t('min')}
|
{Math.round(maxDurationSec / 60)} {t('min')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1353,11 +1389,11 @@ export function CreateAgnetDeploymentSheet({
|
|||||||
|
|
||||||
{/* Power-user disclosure: raw payload. Folded by default so
|
{/* Power-user disclosure: raw payload. Folded by default so
|
||||||
the structured five-section card stays the primary view. */}
|
the structured five-section card stays the primary view. */}
|
||||||
<details className='rounded-lg border bg-muted/20 p-2 text-xs'>
|
<details className='bg-muted/20 rounded-lg border p-2 text-xs'>
|
||||||
<summary className='cursor-pointer select-none text-muted-foreground'>
|
<summary className='text-muted-foreground cursor-pointer select-none'>
|
||||||
{t('Show raw payload (advanced)')}
|
{t('Show raw payload (advanced)')}
|
||||||
</summary>
|
</summary>
|
||||||
<pre className='mt-2 max-h-[300px] overflow-auto rounded bg-background/60 p-3 font-mono text-[10px] leading-relaxed'>
|
<pre className='bg-background/60 mt-2 max-h-[300px] overflow-auto rounded p-3 font-mono text-[10px] leading-relaxed'>
|
||||||
{JSON.stringify(previewPlan, null, 2)}
|
{JSON.stringify(previewPlan, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
+59
-10
@@ -66,6 +66,7 @@ import {
|
|||||||
listAgnetDeployments,
|
listAgnetDeployments,
|
||||||
rejectAgnetApproval,
|
rejectAgnetApproval,
|
||||||
revokeAgnetCredentialLease,
|
revokeAgnetCredentialLease,
|
||||||
|
simulateAgnetDeploymentEvents,
|
||||||
type AgnetApprovalRequest,
|
type AgnetApprovalRequest,
|
||||||
type AgnetCredentialLease,
|
type AgnetCredentialLease,
|
||||||
type AgnetDeployment,
|
type AgnetDeployment,
|
||||||
@@ -234,6 +235,10 @@ function describeRiskLevel(dep: AgnetDeployment): {
|
|||||||
return { label: 'low', tone: 'low' }
|
return { label: 'low', tone: 'low' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function describeSubMode(dep: AgnetDeployment): string {
|
||||||
|
return dep.sub_mode || dep.orchestration_plan?.sub_mode || 'agile'
|
||||||
|
}
|
||||||
|
|
||||||
function describeBudget(dep: AgnetDeployment): string {
|
function describeBudget(dep: AgnetDeployment): string {
|
||||||
const budget = dep.orchestration_plan?.budget
|
const budget = dep.orchestration_plan?.budget
|
||||||
if (!budget) {
|
if (!budget) {
|
||||||
@@ -342,9 +347,25 @@ function grantStatusToneClass(status: string | undefined): string {
|
|||||||
|
|
||||||
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const phase = dep.phase || dep.status
|
const phase = dep.phase || dep.status
|
||||||
const risk = describeRiskLevel(dep)
|
const risk = describeRiskLevel(dep)
|
||||||
const grants = collectResourceGrants(dep)
|
const grants = collectResourceGrants(dep)
|
||||||
|
const simulateMutation = useMutation({
|
||||||
|
mutationFn: () => simulateAgnetDeploymentEvents(dep.deployment_id),
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: ['agnet', 'deployment-events', dep.deployment_id],
|
||||||
|
})
|
||||||
|
toast.success(t('Simulated events recorded'))
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : t('Failed to simulate events')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
// M5 — permission manifest 折叠预览. Default folded per product docs
|
// M5 — permission manifest 折叠预览. Default folded per product docs
|
||||||
// §10 "高级用户可以展开 manifest 预览,但默认折叠". Visible cell
|
// §10 "高级用户可以展开 manifest 预览,但默认折叠". Visible cell
|
||||||
// count keeps the page calm; the toggle reveals the per-grant table.
|
// count keeps the page calm; the toggle reveals the per-grant table.
|
||||||
@@ -366,11 +387,29 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
|
|||||||
t('No objective')}
|
t('No objective')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex shrink-0 items-center gap-2'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
className='h-8 gap-1 rounded-xl text-xs'
|
||||||
|
disabled={simulateMutation.isPending}
|
||||||
|
onClick={() => simulateMutation.mutate()}
|
||||||
|
>
|
||||||
|
<Rocket className='h-3.5 w-3.5' />
|
||||||
|
{t('Simulate')}
|
||||||
|
</Button>
|
||||||
<StatusBadge phase={phase} />
|
<StatusBadge phase={phase} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-5'>
|
<div className='mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-6'>
|
||||||
<MetaPill icon={Activity} label={t('status')} value={phase || '—'} />
|
<MetaPill icon={Activity} label={t('status')} value={phase || '—'} />
|
||||||
|
<MetaPill
|
||||||
|
icon={Rocket}
|
||||||
|
label={t('mode')}
|
||||||
|
value={describeSubMode(dep)}
|
||||||
|
/>
|
||||||
<MetaPill icon={Tag} label={t('risk')} value={risk.label} />
|
<MetaPill icon={Tag} label={t('risk')} value={risk.label} />
|
||||||
<MetaPill
|
<MetaPill
|
||||||
icon={Coins}
|
icon={Coins}
|
||||||
@@ -1202,7 +1241,7 @@ function AgnetApprovalCard({
|
|||||||
onReject: () => void
|
onReject: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<article className='rounded-xl border border-border/70 bg-background/45 p-3'>
|
<article className='border-border/70 bg-background/45 rounded-xl border p-3'>
|
||||||
<div className='flex flex-wrap items-start justify-between gap-3'>
|
<div className='flex flex-wrap items-start justify-between gap-3'>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<p className='font-mono text-xs font-semibold break-all'>
|
<p className='font-mono text-xs font-semibold break-all'>
|
||||||
@@ -1255,7 +1294,7 @@ function AgnetLeaseCard({
|
|||||||
onRevoke: () => void
|
onRevoke: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<article className='rounded-xl border border-border/70 bg-background/45 p-3'>
|
<article className='border-border/70 bg-background/45 rounded-xl border p-3'>
|
||||||
<div className='flex flex-wrap items-start justify-between gap-3'>
|
<div className='flex flex-wrap items-start justify-between gap-3'>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<p className='font-mono text-xs font-semibold break-all'>
|
<p className='font-mono text-xs font-semibold break-all'>
|
||||||
@@ -1304,7 +1343,9 @@ export function AgnetAuditPage() {
|
|||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: ['agnet', 'credential-leases'],
|
queryKey: ['agnet', 'credential-leases'],
|
||||||
})
|
})
|
||||||
void queryClient.invalidateQueries({ queryKey: ['heicode', 'agnet', 'audit'] })
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: ['heicode', 'agnet', 'audit'],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const approveMutation = useMutation({
|
const approveMutation = useMutation({
|
||||||
@@ -1396,12 +1437,16 @@ export function AgnetAuditPage() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<section className='grid gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]'>
|
<section className='grid gap-3 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]'>
|
||||||
<div className='rounded-xl border border-border/70 bg-card/70 p-4'>
|
<div className='border-border/70 bg-card/70 rounded-xl border p-4'>
|
||||||
<div className='mb-3 flex items-center justify-between gap-3'>
|
<div className='mb-3 flex items-center justify-between gap-3'>
|
||||||
<div>
|
<div>
|
||||||
<h3 className='text-sm font-semibold'>{t('Pending approvals')}</h3>
|
<h3 className='text-sm font-semibold'>
|
||||||
|
{t('Pending approvals')}
|
||||||
|
</h3>
|
||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
{t('Approve or reject high-risk Agnet operations before credentials are leased.')}
|
{t(
|
||||||
|
'Approve or reject high-risk Agnet operations before credentials are leased.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge phase='pending' />
|
<StatusBadge phase='pending' />
|
||||||
@@ -1429,12 +1474,16 @@ export function AgnetAuditPage() {
|
|||||||
</div>
|
</div>
|
||||||
</QueryState>
|
</QueryState>
|
||||||
</div>
|
</div>
|
||||||
<div className='rounded-xl border border-border/70 bg-card/70 p-4'>
|
<div className='border-border/70 bg-card/70 rounded-xl border p-4'>
|
||||||
<div className='mb-3 flex items-center justify-between gap-3'>
|
<div className='mb-3 flex items-center justify-between gap-3'>
|
||||||
<div>
|
<div>
|
||||||
<h3 className='text-sm font-semibold'>{t('Active credential leases')}</h3>
|
<h3 className='text-sm font-semibold'>
|
||||||
|
{t('Active credential leases')}
|
||||||
|
</h3>
|
||||||
<p className='text-muted-foreground text-xs'>
|
<p className='text-muted-foreground text-xs'>
|
||||||
{t('Only short-lived lease references are shown. Revoke after the task ends.')}
|
{t(
|
||||||
|
'Only short-lived lease references are shown. Revoke after the task ends.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ShieldCheck className='text-primary size-4' />
|
<ShieldCheck className='text-primary size-4' />
|
||||||
|
|||||||
+140
-42
@@ -11,10 +11,8 @@
|
|||||||
* Forbidden per §10 高级展开: no JSON editor, no permission manifest, no
|
* Forbidden per §10 高级展开: no JSON editor, no permission manifest, no
|
||||||
* resource_grant editor. Only the user-facing summary fields.
|
* resource_grant editor. Only the user-facing summary fields.
|
||||||
*/
|
*/
|
||||||
import { Link, getRouteApi } from '@tanstack/react-router'
|
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { Link, getRouteApi } from '@tanstack/react-router'
|
||||||
import { toast } from 'sonner'
|
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
@@ -32,17 +30,24 @@ import {
|
|||||||
Wallet,
|
Wallet,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { toast } from 'sonner'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import {
|
import {
|
||||||
answerHeicodeTask,
|
answerHeicodeTask,
|
||||||
|
createDeploymentDraftFromHeicodeTask,
|
||||||
getHeicodeTask,
|
getHeicodeTask,
|
||||||
type HeicodeFollowup,
|
type HeicodeFollowup,
|
||||||
type HeicodeManagerAction,
|
type HeicodeManagerAction,
|
||||||
type HeicodeTask,
|
type HeicodeTask,
|
||||||
type HeicodeTaskStatus,
|
type HeicodeTaskStatus,
|
||||||
} from '@/lib/heicode-mcp'
|
} from '@/lib/heicode-mcp'
|
||||||
|
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'
|
||||||
|
|
||||||
const route = getRouteApi('/_authenticated/tasks/$id')
|
const route = getRouteApi('/_authenticated/tasks/$id')
|
||||||
|
|
||||||
@@ -86,7 +91,7 @@ function StatusBadge({ status }: { status: HeicodeTaskStatus | string }) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.12em] ring-1 ring-inset',
|
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-[0.12em] uppercase ring-1 ring-inset',
|
||||||
p.cls
|
p.cls
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -99,7 +104,8 @@ function StatusBadge({ status }: { status: HeicodeTaskStatus | string }) {
|
|||||||
function readScopeArray(card: HeicodeTask['card'], key: string): string[] {
|
function readScopeArray(card: HeicodeTask['card'], key: string): string[] {
|
||||||
if (!card) return []
|
if (!card) return []
|
||||||
const v = (card as Record<string, unknown>)[key]
|
const v = (card as Record<string, unknown>)[key]
|
||||||
if (Array.isArray(v)) return v.filter((x): x is string => typeof x === 'string')
|
if (Array.isArray(v))
|
||||||
|
return v.filter((x): x is string => typeof x === 'string')
|
||||||
if (typeof v === 'string' && v.trim()) return [v]
|
if (typeof v === 'string' && v.trim()) return [v]
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -109,7 +115,8 @@ function readManagerActions(card: HeicodeTask['card']): HeicodeManagerAction[] {
|
|||||||
const v = (card as Record<string, unknown>).manager_actions
|
const v = (card as Record<string, unknown>).manager_actions
|
||||||
if (!Array.isArray(v)) return []
|
if (!Array.isArray(v)) return []
|
||||||
return v
|
return v
|
||||||
.filter((x): x is HeicodeManagerAction =>
|
.filter(
|
||||||
|
(x): x is HeicodeManagerAction =>
|
||||||
typeof x === 'object' &&
|
typeof x === 'object' &&
|
||||||
x != null &&
|
x != null &&
|
||||||
typeof (x as Record<string, unknown>).label === 'string' &&
|
typeof (x as Record<string, unknown>).label === 'string' &&
|
||||||
@@ -121,7 +128,12 @@ function readManagerActions(card: HeicodeTask['card']): HeicodeManagerAction[] {
|
|||||||
/** Resolve helper icon for a manager action by deeplink keyword. */
|
/** Resolve helper icon for a manager action by deeplink keyword. */
|
||||||
function iconForAction(deeplink: string): typeof GitBranch {
|
function iconForAction(deeplink: string): typeof GitBranch {
|
||||||
const k = deeplink.toLowerCase()
|
const k = deeplink.toLowerCase()
|
||||||
if (k.includes('resource') || k.includes('preparation') || k.includes('sk-source')) return GitBranch
|
if (
|
||||||
|
k.includes('resource') ||
|
||||||
|
k.includes('preparation') ||
|
||||||
|
k.includes('sk-source')
|
||||||
|
)
|
||||||
|
return GitBranch
|
||||||
if (k.includes('audit')) return ShieldCheck
|
if (k.includes('audit')) return ShieldCheck
|
||||||
if (k.includes('wallet') || k.includes('budget')) return Wallet
|
if (k.includes('wallet') || k.includes('budget')) return Wallet
|
||||||
if (k.includes('event') || k.includes('activity')) return ScrollText
|
if (k.includes('event') || k.includes('activity')) return ScrollText
|
||||||
@@ -143,7 +155,11 @@ function normalizeDeeplink(deeplink: string): string {
|
|||||||
function collectOpenFollowups(task: HeicodeTask): HeicodeFollowup[] {
|
function collectOpenFollowups(task: HeicodeTask): HeicodeFollowup[] {
|
||||||
for (let i = task.thread.length - 1; i >= 0; i--) {
|
for (let i = task.thread.length - 1; i >= 0; i--) {
|
||||||
const entry = task.thread[i]!
|
const entry = task.thread[i]!
|
||||||
if (entry.kind === 'heicode' && entry.followups && entry.followups.length > 0) {
|
if (
|
||||||
|
entry.kind === 'heicode' &&
|
||||||
|
entry.followups &&
|
||||||
|
entry.followups.length > 0
|
||||||
|
) {
|
||||||
return entry.followups
|
return entry.followups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,13 +187,50 @@ export function TaskCardView() {
|
|||||||
answerHeicodeTask(id, qid, oid),
|
answerHeicodeTask(id, qid, oid),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
queryClient.setQueryData(['heicode', 'task', id], updated)
|
queryClient.setQueryData(['heicode', 'task', id], updated)
|
||||||
void queryClient.invalidateQueries({ queryKey: ['heicode', 'tasks', 'recent'] })
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: ['heicode', 'tasks', 'recent'],
|
||||||
|
})
|
||||||
if (updated.status === 'running') {
|
if (updated.status === 'running') {
|
||||||
toast.success(t('All follow-ups answered. Heicode generated the recommendation summary.'))
|
toast.success(
|
||||||
|
t(
|
||||||
|
'All follow-ups answered. Heicode generated the recommendation summary.'
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(err instanceof Error ? err.message : t('Failed to answer follow-up'))
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : t('Failed to answer follow-up')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deploymentMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!task) throw new Error('Task not found')
|
||||||
|
const draft = await createDeploymentDraftFromHeicodeTask(task, {
|
||||||
|
sub_mode: 'agile',
|
||||||
|
binding_scope: `task-${task.id}`,
|
||||||
|
role_templates: ['backend'],
|
||||||
|
default_model_id: 'agnet-model-builder',
|
||||||
|
})
|
||||||
|
return createAgnetDeployment({
|
||||||
|
orchestration_plan:
|
||||||
|
draft.orchestration_plan as unknown as AgnetOrchestrationPlan,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: (deployment) => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['agnet', 'deployments'] })
|
||||||
|
toast.success(
|
||||||
|
t('Manager deployment created', {
|
||||||
|
deployment_id: deployment.deployment_id,
|
||||||
|
}) as string
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : t('Deployment request failed')
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -201,8 +254,10 @@ export function TaskCardView() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div className='rounded-2xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-10 text-center'>
|
<div className='rounded-2xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-10 text-center'>
|
||||||
<p className='text-sm text-muted-foreground'>
|
<p className='text-muted-foreground text-sm'>
|
||||||
{t('Task not found. It may have been removed or was never created.')}
|
{t(
|
||||||
|
'Task not found. It may have been removed or was never created.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +269,8 @@ export function TaskCardView() {
|
|||||||
// { goal: string, scope: string[], generated_artifacts: string[],
|
// { goal: string, scope: string[], generated_artifacts: string[],
|
||||||
// manager_actions: Array<{label, deeplink}> }
|
// manager_actions: Array<{label, deeplink}> }
|
||||||
const objective =
|
const objective =
|
||||||
(task.card && (task.card as Record<string, unknown>).goal as string | undefined) ||
|
(task.card &&
|
||||||
|
((task.card as Record<string, unknown>).goal as string | undefined)) ||
|
||||||
task.name ||
|
task.name ||
|
||||||
task.intent ||
|
task.intent ||
|
||||||
t('No objective')
|
t('No objective')
|
||||||
@@ -246,7 +302,7 @@ export function TaskCardView() {
|
|||||||
{t('Back to home')}
|
{t('Back to home')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<span className='font-mono text-[11px] text-muted-foreground'>
|
<span className='text-muted-foreground font-mono text-[11px]'>
|
||||||
{task.id}
|
{task.id}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
@@ -269,11 +325,14 @@ export function TaskCardView() {
|
|||||||
<MessageSquare className='h-4 w-4' />
|
<MessageSquare className='h-4 w-4' />
|
||||||
</span>
|
</span>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||||
{t('Heicode is asking')}
|
{t('Heicode is asking')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-1 text-sm text-foreground'>
|
<p className='text-foreground mt-1 text-sm'>
|
||||||
{task.status_caption || t('Answer a few questions so Heicode can draft the right plan.')}
|
{task.status_caption ||
|
||||||
|
t(
|
||||||
|
'Answer a few questions so Heicode can draft the right plan.'
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,7 +341,7 @@ export function TaskCardView() {
|
|||||||
{openFollowups.map((q) => (
|
{openFollowups.map((q) => (
|
||||||
<li
|
<li
|
||||||
key={q.id}
|
key={q.id}
|
||||||
className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-4'
|
className='bg-background/40 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] p-4'
|
||||||
>
|
>
|
||||||
<p className='text-sm font-medium'>{q.question}</p>
|
<p className='text-sm font-medium'>{q.question}</p>
|
||||||
<div className='mt-3 flex flex-wrap gap-2'>
|
<div className='mt-3 flex flex-wrap gap-2'>
|
||||||
@@ -293,7 +352,9 @@ export function TaskCardView() {
|
|||||||
size='sm'
|
size='sm'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
disabled={answerMutation.isPending}
|
disabled={answerMutation.isPending}
|
||||||
onClick={() => answerMutation.mutate({ qid: q.id, oid: opt.id })}
|
onClick={() =>
|
||||||
|
answerMutation.mutate({ qid: q.id, oid: opt.id })
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-xl text-xs',
|
'rounded-xl text-xs',
|
||||||
opt.risk === 'high-risk' &&
|
opt.risk === 'high-risk' &&
|
||||||
@@ -302,7 +363,7 @@ export function TaskCardView() {
|
|||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
{opt.risk === 'high-risk' && (
|
{opt.risk === 'high-risk' && (
|
||||||
<span className='ms-1 rounded-full bg-rose-500/15 px-1.5 text-[9px] uppercase tracking-wider text-rose-300'>
|
<span className='ms-1 rounded-full bg-rose-500/15 px-1.5 text-[9px] tracking-wider text-rose-300 uppercase'>
|
||||||
{t('high-risk')}
|
{t('high-risk')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -326,44 +387,46 @@ export function TaskCardView() {
|
|||||||
>
|
>
|
||||||
<div className='flex items-start justify-between gap-4'>
|
<div className='flex items-start justify-between gap-4'>
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||||
{t('Task card')}
|
{t('Task card')}
|
||||||
</p>
|
</p>
|
||||||
<h1 className='mt-2 text-2xl font-semibold tracking-tight sm:text-3xl'>
|
<h1 className='mt-2 text-2xl font-semibold tracking-tight sm:text-3xl'>
|
||||||
{objective}
|
{objective}
|
||||||
</h1>
|
</h1>
|
||||||
{task.intent && task.intent !== objective && (
|
{task.intent && task.intent !== objective && (
|
||||||
<p className='mt-2 text-sm text-muted-foreground'>{task.intent}</p>
|
<p className='text-muted-foreground mt-2 text-sm'>
|
||||||
|
{task.intent}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge status={task.status} />
|
<StatusBadge status={task.status} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-6 grid gap-5 md:grid-cols-2'>
|
<div className='mt-6 grid gap-5 md:grid-cols-2'>
|
||||||
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-4'>
|
<div className='bg-background/40 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] p-4'>
|
||||||
<p className='flex items-center gap-1.5 text-sm font-semibold'>
|
<p className='flex items-center gap-1.5 text-sm font-semibold'>
|
||||||
<ListChecks className='h-4 w-4 text-primary' />
|
<ListChecks className='text-primary h-4 w-4' />
|
||||||
{t('First-version scope')}
|
{t('First-version scope')}
|
||||||
</p>
|
</p>
|
||||||
<ul className='mt-2 space-y-1.5 text-sm text-muted-foreground'>
|
<ul className='text-muted-foreground mt-2 space-y-1.5 text-sm'>
|
||||||
{firstVersionScope.map((line, i) => (
|
{firstVersionScope.map((line, i) => (
|
||||||
<li key={i} className='flex items-start gap-2'>
|
<li key={i} className='flex items-start gap-2'>
|
||||||
<span className='mt-1.5 inline-block h-1 w-1 shrink-0 rounded-full bg-primary' />
|
<span className='bg-primary mt-1.5 inline-block h-1 w-1 shrink-0 rounded-full' />
|
||||||
<span>{line}</span>
|
<span>{line}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-background/40 p-4'>
|
<div className='bg-background/40 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] p-4'>
|
||||||
<p className='flex items-center gap-1.5 text-sm font-semibold'>
|
<p className='flex items-center gap-1.5 text-sm font-semibold'>
|
||||||
<Sparkles className='h-4 w-4 text-primary' />
|
<Sparkles className='text-primary h-4 w-4' />
|
||||||
{t('Heicode auto-generates')}
|
{t('Heicode auto-generates')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-2 text-sm text-muted-foreground'>
|
<p className='text-muted-foreground mt-2 text-sm'>
|
||||||
{autoGenerated.join(' / ')}
|
{autoGenerated.join(' / ')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-2 text-[11px] text-muted-foreground'>
|
<p className='text-muted-foreground mt-2 text-[11px]'>
|
||||||
{t(
|
{t(
|
||||||
'These artifacts appear inside the desktop client as the task progresses.'
|
'These artifacts appear inside the desktop client as the task progresses.'
|
||||||
)}
|
)}
|
||||||
@@ -371,8 +434,8 @@ export function TaskCardView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-5 rounded-2xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-background/30 p-4'>
|
<div className='bg-background/30 mt-5 rounded-2xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] p-4'>
|
||||||
<p className='text-[11px] font-semibold tracking-[0.14em] text-muted-foreground uppercase'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.14em] uppercase'>
|
||||||
{t('Needs Manager assistance')}
|
{t('Needs Manager assistance')}
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-3 flex flex-wrap gap-2'>
|
<div className='mt-3 flex flex-wrap gap-2'>
|
||||||
@@ -397,19 +460,34 @@ export function TaskCardView() {
|
|||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
<Button
|
||||||
|
asChild
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
className='rounded-xl'
|
||||||
|
>
|
||||||
<Link to='/sk-sources'>
|
<Link to='/sk-sources'>
|
||||||
<GitBranch className='mr-1 h-3.5 w-3.5' />
|
<GitBranch className='mr-1 h-3.5 w-3.5' />
|
||||||
{t('Open preparation checklist')}
|
{t('Open preparation checklist')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
<Button
|
||||||
|
asChild
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
className='rounded-xl'
|
||||||
|
>
|
||||||
<Link to='/audit'>
|
<Link to='/audit'>
|
||||||
<ShieldCheck className='mr-1 h-3.5 w-3.5' />
|
<ShieldCheck className='mr-1 h-3.5 w-3.5' />
|
||||||
{t('Audit & approvals')}
|
{t('Audit & approvals')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild size='sm' variant='outline' className='rounded-xl'>
|
<Button
|
||||||
|
asChild
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
className='rounded-xl'
|
||||||
|
>
|
||||||
<Link to='/wallet'>
|
<Link to='/wallet'>
|
||||||
<Wallet className='mr-1 h-3.5 w-3.5' />
|
<Wallet className='mr-1 h-3.5 w-3.5' />
|
||||||
{t('Budget & usage')}
|
{t('Budget & usage')}
|
||||||
@@ -424,13 +502,31 @@ export function TaskCardView() {
|
|||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='gap-1 text-muted-foreground'
|
className='text-muted-foreground gap-1'
|
||||||
disabled
|
disabled
|
||||||
title={t('Editing the objective happens in the desktop client.')}
|
title={t('Editing the objective happens in the desktop client.')}
|
||||||
>
|
>
|
||||||
<PencilLine className='h-3.5 w-3.5' />
|
<PencilLine className='h-3.5 w-3.5' />
|
||||||
{t('Edit objective in desktop client')}
|
{t('Edit objective in desktop client')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
className='gap-1 rounded-xl'
|
||||||
|
disabled={
|
||||||
|
deploymentMutation.isPending ||
|
||||||
|
task.status === 'failed' ||
|
||||||
|
task.status === 'draft' ||
|
||||||
|
task.status === 'configuring'
|
||||||
|
}
|
||||||
|
onClick={() => deploymentMutation.mutate()}
|
||||||
|
>
|
||||||
|
<Rocket className='h-3.5 w-3.5' />
|
||||||
|
{deploymentMutation.isPending
|
||||||
|
? t('Creating deployment')
|
||||||
|
: t('Create Manager deployment')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size='sm'
|
size='sm'
|
||||||
@@ -450,10 +546,12 @@ export function TaskCardView() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4'>
|
<section className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4'>
|
||||||
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
<p className='text-muted-foreground text-[11px] font-semibold tracking-[0.16em] uppercase'>
|
||||||
{t('Pending context')}
|
{t('Pending context')}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-2 text-sm text-muted-foreground'>{pendingContextLine}</p>
|
<p className='text-muted-foreground mt-2 text-sm'>
|
||||||
|
{pendingContextLine}
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+153
-43
@@ -1,3 +1,5 @@
|
|||||||
|
import { api } from '@/lib/api'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mcp-server (Heicode Manager backend at apimtaiji.azure-api.net/api/mcp)
|
* mcp-server (Heicode Manager backend at apimtaiji.azure-api.net/api/mcp)
|
||||||
* typed client.
|
* typed client.
|
||||||
@@ -34,10 +36,7 @@ function readToken(): string {
|
|||||||
return window.localStorage.getItem(ACCESS_TOKEN_KEY) || ''
|
return window.localStorage.getItem(ACCESS_TOKEN_KEY) || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mcpFetch<T>(
|
async function mcpFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
path: string,
|
|
||||||
init: RequestInit = {}
|
|
||||||
): Promise<T> {
|
|
||||||
const token = readToken()
|
const token = readToken()
|
||||||
const requestId = `heicode-mcp-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
const requestId = `heicode-mcp-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||||
const res = await fetch(`${MCP_BASE}${path}`, {
|
const res = await fetch(`${MCP_BASE}${path}`, {
|
||||||
@@ -59,9 +58,9 @@ async function mcpFetch<T>(
|
|||||||
data = null
|
data = null
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const detail =
|
const detail = (
|
||||||
(data as { detail?: string | { message?: string; code?: string } } | null)
|
data as { detail?: string | { message?: string; code?: string } } | null
|
||||||
?.detail
|
)?.detail
|
||||||
const message =
|
const message =
|
||||||
typeof detail === 'string'
|
typeof detail === 'string'
|
||||||
? detail
|
? detail
|
||||||
@@ -136,7 +135,27 @@ export type HeicodeTask = {
|
|||||||
|
|
||||||
type Envelope<T> = { success: boolean; data?: T; message?: string }
|
type Envelope<T> = { success: boolean; data?: T; message?: string }
|
||||||
|
|
||||||
export async function createTaskFromIntent(intent: string, name?: string): Promise<HeicodeTask> {
|
export type HeicodeTaskDeploymentDraftOptions = {
|
||||||
|
sub_mode?: 'agile' | 'waterfall'
|
||||||
|
binding_scope?: string
|
||||||
|
role_templates?: string[]
|
||||||
|
default_model_id?: string
|
||||||
|
budget?: {
|
||||||
|
max_tokens: number
|
||||||
|
max_cost_usd: number
|
||||||
|
max_duration_sec: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HeicodeTaskDeploymentDraft = {
|
||||||
|
task_id: string
|
||||||
|
orchestration_plan: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTaskFromIntent(
|
||||||
|
intent: string,
|
||||||
|
name?: string
|
||||||
|
): Promise<HeicodeTask> {
|
||||||
const env = await mcpFetch<Envelope<HeicodeTask>>('/api/user/tasks/intent', {
|
const env = await mcpFetch<Envelope<HeicodeTask>>('/api/user/tasks/intent', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ intent, ...(name ? { name } : {}) }),
|
body: JSON.stringify({ intent, ...(name ? { name } : {}) }),
|
||||||
@@ -151,21 +170,33 @@ export async function listHeicodeTasks(params?: {
|
|||||||
status?: HeicodeTaskStatus
|
status?: HeicodeTaskStatus
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<{ items: HeicodeTask[]; total: number; offset: number; limit: number }> {
|
}): Promise<{
|
||||||
|
items: HeicodeTask[]
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}> {
|
||||||
const qs = new URLSearchParams()
|
const qs = new URLSearchParams()
|
||||||
if (params?.status) qs.set('status', params.status)
|
if (params?.status) qs.set('status', params.status)
|
||||||
if (params?.limit != null) qs.set('limit', String(params.limit))
|
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||||
if (params?.offset != null) qs.set('offset', String(params.offset))
|
if (params?.offset != null) qs.set('offset', String(params.offset))
|
||||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||||
const env = await mcpFetch<Envelope<{ items: HeicodeTask[]; total: number; offset: number; limit: number }>>(
|
const env = await mcpFetch<
|
||||||
`/api/user/tasks${suffix}`
|
Envelope<{
|
||||||
)
|
items: HeicodeTask[]
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}>
|
||||||
|
>(`/api/user/tasks${suffix}`)
|
||||||
return env.data ?? { items: [], total: 0, offset: 0, limit: 0 }
|
return env.data ?? { items: [], total: 0, offset: 0, limit: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHeicodeTask(id: string): Promise<HeicodeTask | null> {
|
export async function getHeicodeTask(id: string): Promise<HeicodeTask | null> {
|
||||||
try {
|
try {
|
||||||
const env = await mcpFetch<Envelope<HeicodeTask>>(`/api/user/tasks/${encodeURIComponent(id)}`)
|
const env = await mcpFetch<Envelope<HeicodeTask>>(
|
||||||
|
`/api/user/tasks/${encodeURIComponent(id)}`
|
||||||
|
)
|
||||||
return env.data ?? null
|
return env.data ?? null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && /NOT_FOUND|404/i.test(e.message)) return null
|
if (e instanceof Error && /NOT_FOUND|404/i.test(e.message)) return null
|
||||||
@@ -191,6 +222,34 @@ export async function answerHeicodeTask(
|
|||||||
return env.data
|
return env.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createDeploymentDraftFromHeicodeTask(
|
||||||
|
task: HeicodeTask,
|
||||||
|
options: HeicodeTaskDeploymentDraftOptions = {}
|
||||||
|
): Promise<HeicodeTaskDeploymentDraft> {
|
||||||
|
const res = await api.post<Envelope<HeicodeTaskDeploymentDraft>>(
|
||||||
|
`/api/agnet/user/tasks/${encodeURIComponent(task.id)}/deployment-draft`,
|
||||||
|
{
|
||||||
|
task,
|
||||||
|
sub_mode: options.sub_mode ?? 'agile',
|
||||||
|
binding_scope: options.binding_scope ?? `task-${task.id}`,
|
||||||
|
role_templates: options.role_templates ?? ['backend'],
|
||||||
|
default_model_id: options.default_model_id ?? 'agnet-model-builder',
|
||||||
|
budget: options.budget ?? {
|
||||||
|
max_tokens: 120000,
|
||||||
|
max_cost_usd: 8,
|
||||||
|
max_duration_sec: 3600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const env = res.data
|
||||||
|
if (!env?.success || !env.data) {
|
||||||
|
throw new Error(
|
||||||
|
env?.message || 'createDeploymentDraftFromHeicodeTask failed'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return env.data
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// §4 NewAPI metadata passthrough — 4 endpoints
|
// §4 NewAPI metadata passthrough — 4 endpoints
|
||||||
// All return Envelope<…>.
|
// All return Envelope<…>.
|
||||||
@@ -210,10 +269,15 @@ export type HeicodeBalance = {
|
|||||||
|
|
||||||
export async function getHeicodeBalance(): Promise<HeicodeBalance | null> {
|
export async function getHeicodeBalance(): Promise<HeicodeBalance | null> {
|
||||||
try {
|
try {
|
||||||
const env = await mcpFetch<Envelope<HeicodeBalance>>('/api/user/heicode/balance')
|
const env = await mcpFetch<Envelope<HeicodeBalance>>(
|
||||||
|
'/api/user/heicode/balance'
|
||||||
|
)
|
||||||
return env.data ?? null
|
return env.data ?? null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && /NOT_CONFIGURED|HEICODE_USER_NOT_FOUND/i.test(e.message)) {
|
if (
|
||||||
|
e instanceof Error &&
|
||||||
|
/NOT_CONFIGURED|HEICODE_USER_NOT_FOUND/i.test(e.message)
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
@@ -227,34 +291,50 @@ export async function getHeicodeModels(): Promise<{
|
|||||||
count: number
|
count: number
|
||||||
} | null> {
|
} | null> {
|
||||||
try {
|
try {
|
||||||
const env = await mcpFetch<Envelope<{
|
const env = await mcpFetch<
|
||||||
|
Envelope<{
|
||||||
heicodeUserId: number
|
heicodeUserId: number
|
||||||
email: string
|
email: string
|
||||||
items: Array<Record<string, unknown>>
|
items: Array<Record<string, unknown>>
|
||||||
count: number
|
count: number
|
||||||
}>>('/api/user/heicode/models')
|
}>
|
||||||
|
>('/api/user/heicode/models')
|
||||||
return env.data ?? null
|
return env.data ?? null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && /NOT_CONFIGURED|HEICODE_USER_NOT_FOUND/i.test(e.message)) {
|
if (
|
||||||
|
e instanceof Error &&
|
||||||
|
/NOT_CONFIGURED|HEICODE_USER_NOT_FOUND/i.test(e.message)
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHeicodeUsage(days = 30): Promise<Array<Record<string, unknown>>> {
|
export async function getHeicodeUsage(
|
||||||
const env = await mcpFetch<Envelope<{
|
days = 30
|
||||||
|
): Promise<Array<Record<string, unknown>>> {
|
||||||
|
const env = await mcpFetch<
|
||||||
|
Envelope<{
|
||||||
items: Array<Record<string, unknown>>
|
items: Array<Record<string, unknown>>
|
||||||
count: number
|
count: number
|
||||||
}>>(`/api/user/heicode/usage?days=${Math.max(1, Math.min(90, days))}`)
|
}>
|
||||||
|
>(`/api/user/heicode/usage?days=${Math.max(1, Math.min(90, days))}`)
|
||||||
return env.data?.items ?? []
|
return env.data?.items ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHeicodeLogs(limit = 50, page = 1): Promise<Array<Record<string, unknown>>> {
|
export async function getHeicodeLogs(
|
||||||
const env = await mcpFetch<Envelope<{
|
limit = 50,
|
||||||
|
page = 1
|
||||||
|
): Promise<Array<Record<string, unknown>>> {
|
||||||
|
const env = await mcpFetch<
|
||||||
|
Envelope<{
|
||||||
items: Array<Record<string, unknown>>
|
items: Array<Record<string, unknown>>
|
||||||
count: number
|
count: number
|
||||||
}>>(`/api/user/heicode/logs?limit=${Math.max(1, Math.min(200, limit))}&page=${Math.max(1, page)}`)
|
}>
|
||||||
|
>(
|
||||||
|
`/api/user/heicode/logs?limit=${Math.max(1, Math.min(200, limit))}&page=${Math.max(1, page)}`
|
||||||
|
)
|
||||||
return env.data?.items ?? []
|
return env.data?.items ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,9 +371,9 @@ export async function listMcpAgnetDeployments(params?: {
|
|||||||
if (params?.binding_scope) qs.set('binding_scope', params.binding_scope)
|
if (params?.binding_scope) qs.set('binding_scope', params.binding_scope)
|
||||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||||
try {
|
try {
|
||||||
const env = await mcpFetch<Envelope<{ items?: McpAgnetDeployment[]; total?: number }>>(
|
const env = await mcpFetch<
|
||||||
`/api/agnet/deployments${suffix}`
|
Envelope<{ items?: McpAgnetDeployment[]; total?: number }>
|
||||||
)
|
>(`/api/agnet/deployments${suffix}`)
|
||||||
return env.data?.items ?? []
|
return env.data?.items ?? []
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
@@ -363,22 +443,34 @@ export async function listResources(params?: {
|
|||||||
status?: ResourceStatus
|
status?: ResourceStatus
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<{ items: ResourceBinding[]; total: number; offset: number; limit: number }> {
|
}): Promise<{
|
||||||
|
items: ResourceBinding[]
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}> {
|
||||||
const qs = new URLSearchParams()
|
const qs = new URLSearchParams()
|
||||||
if (params?.type) qs.set('type', params.type)
|
if (params?.type) qs.set('type', params.type)
|
||||||
if (params?.status) qs.set('status', params.status)
|
if (params?.status) qs.set('status', params.status)
|
||||||
if (params?.limit != null) qs.set('limit', String(params.limit))
|
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||||
if (params?.offset != null) qs.set('offset', String(params.offset))
|
if (params?.offset != null) qs.set('offset', String(params.offset))
|
||||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||||
const env = await mcpFetch<Envelope<{ items: ResourceBinding[]; total: number; offset: number; limit: number }>>(
|
const env = await mcpFetch<
|
||||||
`/api/resources${suffix}`
|
Envelope<{
|
||||||
)
|
items: ResourceBinding[]
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}>
|
||||||
|
>(`/api/resources${suffix}`)
|
||||||
return env.data ?? { items: [], total: 0, offset: 0, limit: 100 }
|
return env.data ?? { items: [], total: 0, offset: 0, limit: 100 }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResource(id: string): Promise<ResourceBinding | null> {
|
export async function getResource(id: string): Promise<ResourceBinding | null> {
|
||||||
try {
|
try {
|
||||||
const env = await mcpFetch<Envelope<ResourceBinding>>(`/api/resources/${encodeURIComponent(id)}`)
|
const env = await mcpFetch<Envelope<ResourceBinding>>(
|
||||||
|
`/api/resources/${encodeURIComponent(id)}`
|
||||||
|
)
|
||||||
return env.data ?? null
|
return env.data ?? null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && /404|NOT_FOUND/i.test(e.message)) return null
|
if (e instanceof Error && /404|NOT_FOUND/i.test(e.message)) return null
|
||||||
@@ -386,7 +478,9 @@ export async function getResource(id: string): Promise<ResourceBinding | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createResource(body: CreateResourceBody): Promise<ResourceBinding> {
|
export async function createResource(
|
||||||
|
body: CreateResourceBody
|
||||||
|
): Promise<ResourceBinding> {
|
||||||
const env = await mcpFetch<Envelope<ResourceBinding>>('/api/resources', {
|
const env = await mcpFetch<Envelope<ResourceBinding>>('/api/resources', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -465,22 +559,36 @@ export async function listResourceGrants(params?: {
|
|||||||
status?: GrantStatus
|
status?: GrantStatus
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<{ items: ResourceGrant[]; total: number; offset: number; limit: number }> {
|
}): Promise<{
|
||||||
|
items: ResourceGrant[]
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}> {
|
||||||
const qs = new URLSearchParams()
|
const qs = new URLSearchParams()
|
||||||
if (params?.resource_id) qs.set('resource_id', params.resource_id)
|
if (params?.resource_id) qs.set('resource_id', params.resource_id)
|
||||||
if (params?.status) qs.set('status', params.status)
|
if (params?.status) qs.set('status', params.status)
|
||||||
if (params?.limit != null) qs.set('limit', String(params.limit))
|
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||||
if (params?.offset != null) qs.set('offset', String(params.offset))
|
if (params?.offset != null) qs.set('offset', String(params.offset))
|
||||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||||
const env = await mcpFetch<Envelope<{ items: ResourceGrant[]; total: number; offset: number; limit: number }>>(
|
const env = await mcpFetch<
|
||||||
`/api/resource-grants${suffix}`
|
Envelope<{
|
||||||
)
|
items: ResourceGrant[]
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
}>
|
||||||
|
>(`/api/resource-grants${suffix}`)
|
||||||
return env.data ?? { items: [], total: 0, offset: 0, limit: 100 }
|
return env.data ?? { items: [], total: 0, offset: 0, limit: 100 }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResourceGrant(id: string): Promise<ResourceGrant | null> {
|
export async function getResourceGrant(
|
||||||
|
id: string
|
||||||
|
): Promise<ResourceGrant | null> {
|
||||||
try {
|
try {
|
||||||
const env = await mcpFetch<Envelope<ResourceGrant>>(`/api/resource-grants/${encodeURIComponent(id)}`)
|
const env = await mcpFetch<Envelope<ResourceGrant>>(
|
||||||
|
`/api/resource-grants/${encodeURIComponent(id)}`
|
||||||
|
)
|
||||||
return env.data ?? null
|
return env.data ?? null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && /404|NOT_FOUND/i.test(e.message)) return null
|
if (e instanceof Error && /404|NOT_FOUND/i.test(e.message)) return null
|
||||||
@@ -488,7 +596,9 @@ export async function getResourceGrant(id: string): Promise<ResourceGrant | null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createResourceGrant(body: CreateGrantBody): Promise<ResourceGrant> {
|
export async function createResourceGrant(
|
||||||
|
body: CreateGrantBody
|
||||||
|
): Promise<ResourceGrant> {
|
||||||
const env = await mcpFetch<Envelope<ResourceGrant>>('/api/resource-grants', {
|
const env = await mcpFetch<Envelope<ResourceGrant>>('/api/resource-grants', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -527,9 +637,9 @@ export async function listMcpAuditLogs(params?: {
|
|||||||
if (params?.limit != null) qs.set('limit', String(params.limit))
|
if (params?.limit != null) qs.set('limit', String(params.limit))
|
||||||
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
const suffix = qs.toString() ? `?${qs.toString()}` : ''
|
||||||
try {
|
try {
|
||||||
const env = await mcpFetch<Envelope<{ items?: McpAuditEntry[]; total?: number }>>(
|
const env = await mcpFetch<
|
||||||
`/api/agnet/audit-logs${suffix}`
|
Envelope<{ items?: McpAuditEntry[]; total?: number }>
|
||||||
)
|
>(`/api/agnet/audit-logs${suffix}`)
|
||||||
return env.data?.items ?? []
|
return env.data?.items ?? []
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
|
|||||||
Reference in New Issue
Block a user