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 && ( + + )}