From 8a6fea235e67069af490481ade55bc30d7443ab7 Mon Sep 17 00:00:00 2001
From: gongzhiyong
Date: Tue, 26 May 2026 18:41:26 +0800
Subject: [PATCH] fix: restrict agnet simulation controls
---
heicode/controller/agnet_control_plane.go | 4 +++
.../controller/agnet_control_plane_test.go | 33 +++++++++++++++++++
.../src/features/agnet-console/pages.tsx | 27 ++++++++-------
3 files changed, 53 insertions(+), 11 deletions(-)
diff --git a/heicode/controller/agnet_control_plane.go b/heicode/controller/agnet_control_plane.go
index 5ed8f576..d8ec870b 100644
--- a/heicode/controller/agnet_control_plane.go
+++ b/heicode/controller/agnet_control_plane.go
@@ -1230,6 +1230,10 @@ func normalizeAgnetSimulationEvents(values []string) []string {
}
func AgnetSimulateUserDeploymentEvents(c *gin.Context) {
+ if c.GetInt("role") < common.RoleAdminUser && !common.GetEnvOrDefaultBool("AGNET_SIMULATION_ENABLED", false) {
+ agnetError(c, "SIMULATION_DISABLED", "simulation endpoint is admin-only unless AGNET_SIMULATION_ENABLED=true")
+ return
+ }
record, ok := requireAuthenticatedUserAgnetDeployment(c)
if !ok {
return
diff --git a/heicode/controller/agnet_control_plane_test.go b/heicode/controller/agnet_control_plane_test.go
index d93d444c..2dbd978e 100644
--- a/heicode/controller/agnet_control_plane_test.go
+++ b/heicode/controller/agnet_control_plane_test.go
@@ -504,6 +504,7 @@ func TestAgnetUserDeploymentSimulatedEventsArePersistedAndPrefixed(t *testing.T)
simRecorder := httptest.NewRecorder()
simCtx, _ := gin.CreateTestContext(simRecorder)
simCtx.Set("id", 7)
+ simCtx.Set("role", common.RoleAdminUser)
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")
@@ -546,6 +547,7 @@ func TestAgnetUserDeploymentSimulationRejectsOtherUsersDeployment(t *testing.T)
simRecorder := httptest.NewRecorder()
simCtx, _ := gin.CreateTestContext(simRecorder)
simCtx.Set("id", 8)
+ simCtx.Set("role", common.RoleAdminUser)
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")
@@ -557,6 +559,37 @@ func TestAgnetUserDeploymentSimulationRejectsOtherUsersDeployment(t *testing.T)
require.Equal(t, "DEPLOYMENT_FORBIDDEN", envelope.Error.Code)
}
+func TestAgnetUserDeploymentSimulationIsDisabledForRegularUserByDefault(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.Set("role", common.RoleCommonUser)
+ 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, "SIMULATION_DISABLED", envelope.Error.Code)
+}
+
func TestAgnetUserSwarmsAdapterCreatesScopedDeployment(t *testing.T) {
setupAgnetControlPlaneTestDB(t)
resetAgnetControlPlaneState(t)
diff --git a/heicode/web/default/src/features/agnet-console/pages.tsx b/heicode/web/default/src/features/agnet-console/pages.tsx
index c9ae4463..2d4aed1c 100644
--- a/heicode/web/default/src/features/agnet-console/pages.tsx
+++ b/heicode/web/default/src/features/agnet-console/pages.tsx
@@ -23,6 +23,7 @@ import {
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
+import { useAuthStore } from '@/stores/auth-store'
import { api } from '@/lib/api'
// /audit pulls from mcp-server §5.10 stub now, not the Heicode-local
// controller — the contract doc names that endpoint as the canonical
@@ -349,6 +350,8 @@ function grantStatusToneClass(status: string | undefined): string {
function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
+ const currentUser = useAuthStore((state) => state.auth.user)
+ const canSimulate = Boolean(currentUser?.role && currentUser.role >= 10)
const phase = dep.phase || dep.status
const risk = describeRiskLevel(dep)
const grants = collectResourceGrants(dep)
@@ -389,17 +392,19 @@ function RunDetailPanel({ dep }: { dep: AgnetDeployment }) {
-
+ {canSimulate && (
+
+ )}