From f45fbfd5f0d0c6d63bbe9d57d9bc804eaf7a4eac Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 7 Jun 2026 16:32:13 +0800 Subject: [PATCH] feat(captain): secure custom tool auth config --- docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md | 11 ++-- .../captain_custom_tool_crud_handler_test.go | 65 +++++++++++++++++++ .../api/v1/captain_custom_tool_handler.go | 45 +++++++++++-- .../captain_custom_tool_test_handler_test.go | 25 +++++++ 4 files changed, 135 insertions(+), 11 deletions(-) diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 59e68cb1..e02c214b 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,11 +49,11 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-07 P3.106 Captain custom tool validation parity, prepared as `feat(captain): validate custom tools`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(captain): validate custom tools`. -- Latest documentation/tooling checkpoint: this tracker update records Chatwoot-compatible Captain custom-tool enum and parameter-schema validation. +- Current tracking checkpoint: 2026-06-07 P3.107 Captain custom tool policy and auth serialization parity, prepared as `feat(captain): secure custom tool auth config`. +- Latest implementation checkpoint: this checkpoint, prepared as `feat(captain): secure custom tool auth config`. +- Latest documentation/tooling checkpoint: this tracker update records Chatwoot-compatible Captain custom-tool policy gates and administrator-only auth-config serialization. - Plan landing status: complete for the current known Hermes plans and user-confirmed scope. Future work should update this file directly instead of opening a parallel tracker. -- Worktree status at this implementation checkpoint: Captain custom-tool create/update now validates persisted fields closer to `Captain::CustomTool`: blank title/endpoint URL, explicit duplicate slug, unsupported `http_method`/`auth_type`, generated slug length, and `param_schema` item shape return raw `422 { message, attributes }` record-invalid responses instead of generic 500s. This retains P3.105 custom-tool create limits and slug parity, P3.104 Captain custom tool feature gating, P5.3i CSAT survey indexing, P5.3h provider webhook indexing, P5.3g message delivery indexing, P5.3f automation action indexing, P5.3e conversation maintenance indexing, P5.3d conversation bulk-action indexing, P5.3c contact label search indexing, P3.103 contact bulk-action parity, and prior checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. +- Worktree status at this implementation checkpoint: Captain custom-tool list/show/create/update now serialize `auth_config` only for administrator/super-admin request roles, while agent roles can list/show tools without secrets and receive raw `403 { error: "You are not authorized to do this action" }` for create/test/update/delete like `Captain::CustomToolPolicy`. This retains P3.106 custom-tool validation parity, P3.105 custom-tool create limits and slug parity, P3.104 Captain custom tool feature gating, P5.3i CSAT survey indexing, P5.3h provider webhook indexing, P5.3g message delivery indexing, P5.3f automation action indexing, P5.3e conversation maintenance indexing, P5.3d conversation bulk-action indexing, P5.3c contact label search indexing, P3.103 contact bulk-action parity, and prior checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. - Next executable implementation checkpoint: continue Phase 2/3 drift audit for the next reused-frontend mismatch, or run B12 live smoke when the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack is available. Re-run Phase 6 placeholder audit after future route/smoke changes. - `go test ./...` passes when run outside the restricted socket sandbox for the latest implementation baseline; the latest docs/tooling checkpoint verified `scripts/parity_frontend_smoke.sh --check` with workspace-local temp/cache dirs after `/tmp` was full. - Route dump succeeds with `972` registered routes after enterprise account route tracking. @@ -156,6 +156,7 @@ This table is the shortest authoritative handoff view. If an older lower section | Priority | Workstream | Current state | Next checkpoint | Commit close rule | | --- | --- | --- | --- | --- | +| 0 | P3.107 Captain custom tool policy and auth serialization parity | Implemented for reused Captain custom-tool security behavior: list/show/create/update now omit `auth_config` unless the request role is administrator/super-admin, while create/test/update/delete enforce the inspected `Captain::CustomToolPolicy` mutating-action administrator gate for authenticated role contexts. Agent roles can still list/show custom tools without auth secrets, but mutating/test actions receive raw `403 { error: "You are not authorized to do this action" }`. | Keep in Review; reopen from B12 Captain custom-tools smoke or fresh reference evidence for exact custom-role policy behavior, account-user lookup when role context is absent, or deeper Pundit authorization ordering beyond the inspected policy/Jbuilder contract. | Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.106 Captain custom tool validation parity | Implemented for reused Captain custom-tool create/update validation: GoChat now maps blank title/endpoint URL, explicit duplicate slug, unsupported `GET`/`POST`-only `http_method`, unsupported `none`/`bearer`/`basic`/`api_key` `auth_type`, generated slug length overflow, and invalid `param_schema` array item shape into Chatwoot-style raw `422 { message, attributes }` responses. `param_schema` items require string `name`, `type`, and `description`, allow boolean `required`, and reject extra keys. | Keep in Review; reopen from B12 Captain custom-tools smoke or fresh reference evidence for exact Rails `JSONSchemer` additional-property wording, title max-length frontend-only enforcement, update clearing semantics for blank fields, or admin-only `auth_config` serializer gating. | Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.105 Captain custom tool create limits and slug parity | Implemented for reused Captain custom-tool creation: GoChat now mirrors `Captain::CustomTool` by allowing at most 15 custom tools per account, returning raw `422 { error: "You can create a maximum of 15 custom tools per account" }` when the cap is exceeded, generating default slugs as `custom_` + parameterized title with underscore separators, capping generated slugs at 64 characters, adding `_xxxxxx` lowercase alphanumeric suffixes on same-account collisions, and keeping the uniqueness boundary scoped to `(account_id, slug)` like the Chatwoot schema. | Keep in Review; reopen from B12 Captain custom-tools smoke or fresh reference evidence for exact Rails `parameterize` locale transliteration, concurrent account-row locking, auth-config administrator-only serialization, enum/schema validation response shape, or slug-generation exhaustion wording beyond the inspected model/controller contract. | Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | | 0 | P3.104 Captain custom tools feature gate | Implemented for reused Captain custom-tool screens: `GET/POST/PUT/DELETE /api/v1/accounts/:account_id/captain/custom_tools` and `POST /captain/custom_tools/test` now enforce the inspected Chatwoot `ensure_custom_tools_enabled` gate. Accounts must have `custom_tools` or `captain_integration_v2` enabled; disabled accounts receive raw `403 { error: "Custom tools are not enabled for this account" }`. Bootstrap passes the account repository into the custom-tool service so normal app routes use persisted account feature flags. | Keep in Review; reopen from B12 Captain custom-tools smoke or fresh reference evidence for exact authorization ordering, feature-flag defaults, custom-tool limit behavior, or request execution drift beyond the inspected controller/frontend contract. | Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | @@ -402,6 +403,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `feat(captain): secure custom tool auth config` | Advances P3.107 with Chatwoot Captain custom-tool policy and serializer parity. GoChat now hides `auth_config` from non-admin list/show/create/update payloads and enforces the inspected administrator-only policy for create/test/update/delete when an authenticated request role is present. | Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.107 to Review; continue Phase 2/3 audit, Phase 6 placeholder audit, or B12 live smoke. | | `feat(captain): validate custom tools` | Advances P3.106 with Chatwoot `Captain::CustomTool` model validation parity. GoChat now returns record-invalid-shaped raw `422 { message, attributes }` responses for blank required fields, explicit duplicate slugs, unsupported custom-tool enums, generated slug length overflow, and invalid `param_schema` items instead of generic internal errors. | Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.106 to Review; continue admin-only custom-tool auth serialization, Phase 2/3 audit, Phase 6 placeholder audit, or B12 live smoke. | | `feat(captain): align custom tool limits` | Advances P3.105 with Chatwoot Captain custom-tool create limits and generated slug parity. GoChat now enforces the 15-tools-per-account cap, returns the inspected limit error as raw `422`, generates default `custom_` underscore slugs capped at 64 characters, adds six-character lowercase alphanumeric collision suffixes, and makes the GORM model uniqueness boundary match the reference `(account_id, slug)` schema. | Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P3.105 to Review; continue Captain custom-tool validation/administrator serialization drift, Phase 2/3 audit, Phase 6 placeholder audit, or B12 live smoke. | | `feat(search): index conversation maintenance` | Advances P5.3/B6 for mandatory Meilisearch background-maintenance parity. Scheduled snooze reopen and account auto-resolution now reindex updated conversations, message-status maintenance reindexes changed messages plus the parent conversation, and bootstrap registers search-aware maintenance handlers after durable search indexing is wired. | Focused conversation maintenance search tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. | Move P5.3e to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. | @@ -2820,3 +2822,4 @@ Verification milestone gates: - 2026-06-07: P3.104 Captain custom-tools feature-gate checkpoint prepared as `feat(captain): gate custom tools`; audited Chatwoot `Api::V1::Accounts::Captain::CustomToolsController#ensure_custom_tools_enabled`, reused dashboard `api/captain/customTools.js`, and GoChat Captain custom-tool routes. GoChat now gates custom-tool index/show/create/update/delete/test routes on persisted account `custom_tools` or `captain_integration_v2` feature flags, returns raw `403 { error: "Custom tools are not enabled for this account" }` for disabled accounts, and wires the account repository into the production Captain custom-tool service. Focused Captain custom-tool/resource handler tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.105 Captain custom-tool create limits and slug checkpoint prepared as `feat(captain): align custom tool limits`; audited Chatwoot `Captain::CustomTool` constants/callbacks, `CustomToolsController#create` limit rescue, and `_custom_tool.json.jbuilder`. GoChat now enforces the 15-tools-per-account cap, returns raw `422` with the reference limit message, generates omitted slugs as `custom_` + underscore-parameterized title, caps generated slugs at 64 characters, appends `_xxxxxx` lowercase alphanumeric suffixes on account-scoped collisions, and aligns the GORM uniqueness tag with the reference `(account_id, slug)` index. Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. - 2026-06-07: P3.106 Captain custom-tool validation checkpoint prepared as `feat(captain): validate custom tools`; audited Chatwoot `Captain::CustomTool` model validations, enum lists, `PARAM_SCHEMA_VALIDATION`, `JsonSchemaValidator`, and `RequestExceptionHandler#render_record_invalid`. GoChat now returns raw `422 { message, attributes }` for blank title/endpoint URL, duplicate explicit slugs, unsupported `http_method`/`auth_type`, overlong generated slugs, and invalid `param_schema` items; schema items require string `name`/`type`/`description`, allow boolean `required`, and reject extra keys. Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. +- 2026-06-07: P3.107 Captain custom-tool policy/auth-config checkpoint prepared as `feat(captain): secure custom tool auth config`; audited Chatwoot `Captain::CustomToolPolicy` and `_custom_tool.json.jbuilder` administrator-only `auth_config` branch. GoChat now omits `auth_config` from non-admin list/show/create/update payloads, exposes it for administrator/super-admin request roles, allows agent roles to list/show tools without secrets, and returns raw `403 { error: "You are not authorized to do this action" }` for agent create/test/update/delete attempts. Focused Captain custom-tool/resource tests passed; full `go test ./...` passed outside the restricted socket sandbox; `git diff --check` passed. No route artifacts change. diff --git a/internal/handler/api/v1/captain_custom_tool_crud_handler_test.go b/internal/handler/api/v1/captain_custom_tool_crud_handler_test.go index a23eeda8..b94a171c 100644 --- a/internal/handler/api/v1/captain_custom_tool_crud_handler_test.go +++ b/internal/handler/api/v1/captain_custom_tool_crud_handler_test.go @@ -65,6 +65,12 @@ func (s *CaptainCustomToolCRUDTestSuite) SetupSuite() { // Unified router: :id = account_id, :tool_id = tool_id s.router = gin.New() s.router.RedirectTrailingSlash = false + s.router.Use(func(c *gin.Context) { + if role := c.GetHeader("X-Test-Role"); role != "" { + c.Set("role", role) + } + c.Next() + }) accGroup := s.router.Group("/api/v1/accounts/:account_id") { ctGroup := accGroup.Group("/captain/custom_tools") @@ -97,6 +103,10 @@ func (s *CaptainCustomToolCRUDTestSuite) accountPath() string { // helper: send HTTP request via unified router func (s *CaptainCustomToolCRUDTestSuite) makeRequest(method, path string, body interface{}) *httptest.ResponseRecorder { + return s.makeRequestWithRole(method, path, body, "") +} + +func (s *CaptainCustomToolCRUDTestSuite) makeRequestWithRole(method, path string, body interface{}, role string) *httptest.ResponseRecorder { var bodyBytes []byte if body != nil { bodyBytes, _ = json.Marshal(body) @@ -106,6 +116,9 @@ func (s *CaptainCustomToolCRUDTestSuite) makeRequest(method, path string, body i if body != nil { req.Header.Set("Content-Type", "application/json") } + if role != "" { + req.Header.Set("X-Test-Role", role) + } s.router.ServeHTTP(w, req) return w } @@ -165,6 +178,58 @@ func (s *CaptainCustomToolCRUDTestSuite) TestCreate_默认GET方法() { assert.Equal(s.T(), "GET", resp["http_method"]) // default HTTP method } +func (s *CaptainCustomToolCRUDTestSuite) TestPayload_AuthConfigOnlyForAdministrators() { + body := map[string]interface{}{ + "custom_tool": map[string]interface{}{ + "title": "Bearer tool", + "endpoint_url": "https://example.com/bearer", + "auth_type": "bearer", + "auth_config": map[string]interface{}{"token": "secret-token"}, + }, + } + + w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body) + assert.Equal(s.T(), http.StatusOK, w.Code) + var created map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &created)) + assert.NotContains(s.T(), created, "auth_config") + toolID := strconv.FormatFloat(created["id"].(float64), 'f', -1, 64) + + w = s.makeRequestWithRole("GET", s.accountPath()+"/captain/custom_tools/"+toolID, nil, "agent") + assert.Equal(s.T(), http.StatusOK, w.Code) + var agentResp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &agentResp)) + assert.NotContains(s.T(), agentResp, "auth_config") + + w = s.makeRequestWithRole("GET", s.accountPath()+"/captain/custom_tools/"+toolID, nil, "administrator") + assert.Equal(s.T(), http.StatusOK, w.Code) + var adminResp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &adminResp)) + assert.Equal(s.T(), map[string]interface{}{"token": "secret-token"}, adminResp["auth_config"]) +} + +func (s *CaptainCustomToolCRUDTestSuite) TestMutations_AgentRoleForbidden() { + toolID := s.createToolAndGetID("Agent forbidden", "agent-forbidden", "https://example.com/agent") + + w := s.makeRequestWithRole("POST", s.accountPath()+"/captain/custom_tools/", map[string]interface{}{ + "custom_tool": map[string]interface{}{ + "title": "Agent create", + "endpoint_url": "https://example.com/create", + }, + }, "agent") + assert.Equal(s.T(), http.StatusForbidden, w.Code) + + w = s.makeRequestWithRole("PUT", s.accountPath()+"/captain/custom_tools/"+toolID, map[string]interface{}{"title": "blocked"}, "agent") + assert.Equal(s.T(), http.StatusForbidden, w.Code) + + w = s.makeRequestWithRole("DELETE", s.accountPath()+"/captain/custom_tools/"+toolID, nil, "agent") + assert.Equal(s.T(), http.StatusForbidden, w.Code) + + var resp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(s.T(), "You are not authorized to do this action", resp["error"]) +} + func (s *CaptainCustomToolCRUDTestSuite) TestCreate_省略Slug时使用ChatwootCustom前缀和下划线() { body := map[string]interface{}{ "custom_tool": map[string]interface{}{ diff --git a/internal/handler/api/v1/captain_custom_tool_handler.go b/internal/handler/api/v1/captain_custom_tool_handler.go index 497dd061..55687954 100644 --- a/internal/handler/api/v1/captain_custom_tool_handler.go +++ b/internal/handler/api/v1/captain_custom_tool_handler.go @@ -33,6 +33,9 @@ func (h *CaptainCustomToolHandler) Create(c *gin.Context) { if !h.ensureCustomToolsEnabled(c, accountID) { return } + if !h.ensureCustomToolAdmin(c) { + return + } var req service.CreateCustomToolRequest if err := bindNestedJSONPayload(c, "custom_tool", &req); err != nil { @@ -54,7 +57,7 @@ func (h *CaptainCustomToolHandler) Create(c *gin.Context) { return } - c.JSON(http.StatusOK, captainCustomToolPayload(tool)) + c.JSON(http.StatusOK, captainCustomToolPayload(c, tool)) } // Get retrieves a custom tool by ID. @@ -81,7 +84,7 @@ func (h *CaptainCustomToolHandler) Get(c *gin.Context) { return } - c.JSON(http.StatusOK, captainCustomToolPayload(tool)) + c.JSON(http.StatusOK, captainCustomToolPayload(c, tool)) } // Update updates an existing custom tool. @@ -95,6 +98,9 @@ func (h *CaptainCustomToolHandler) Update(c *gin.Context) { if !h.ensureCustomToolsEnabled(c, accountID) { return } + if !h.ensureCustomToolAdmin(c) { + return + } id, err := parseUintAnyParam(c, "tool_id", "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") @@ -117,7 +123,7 @@ func (h *CaptainCustomToolHandler) Update(c *gin.Context) { return } - c.JSON(http.StatusOK, captainCustomToolPayload(tool)) + c.JSON(http.StatusOK, captainCustomToolPayload(c, tool)) } // Delete deletes a custom tool. @@ -131,6 +137,9 @@ func (h *CaptainCustomToolHandler) Delete(c *gin.Context) { if !h.ensureCustomToolsEnabled(c, accountID) { return } + if !h.ensureCustomToolAdmin(c) { + return + } id, err := parseUintAnyParam(c, "tool_id", "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id") @@ -167,7 +176,7 @@ func (h *CaptainCustomToolHandler) List(c *gin.Context) { payload := make([]gin.H, 0, len(tools)) for i := range tools { - payload = append(payload, captainCustomToolPayload(&tools[i])) + payload = append(payload, captainCustomToolPayload(c, &tools[i])) } c.JSON(http.StatusOK, gin.H{"payload": payload, "meta": gin.H{"total_count": count, "page": 1}}) } @@ -208,6 +217,9 @@ func (h *CaptainCustomToolHandler) TestTool(c *gin.Context) { if !h.ensureCustomToolsEnabled(c, accountID) { return } + if !h.ensureCustomToolAdmin(c) { + return + } var req service.TestToolRequest if err := bindNestedJSONPayload(c, "custom_tool", &req); err != nil { @@ -233,6 +245,17 @@ func (h *CaptainCustomToolHandler) ensureCustomToolsEnabled(c *gin.Context, acco return false } +func (h *CaptainCustomToolHandler) ensureCustomToolAdmin(c *gin.Context) bool { + if role, exists := c.Get("role"); exists { + if role == "administrator" || role == "super_admin" { + return true + } + c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to do this action"}) + return false + } + return true +} + func renderCaptainCustomToolValidationError(c *gin.Context, err error) bool { var validationErr *service.CaptainCustomToolValidationError if !errors.As(err, &validationErr) { @@ -245,8 +268,8 @@ func renderCaptainCustomToolValidationError(c *gin.Context, err error) bool { return true } -func captainCustomToolPayload(tool *model.CaptainCustomTool) gin.H { - return gin.H{ +func captainCustomToolPayload(c *gin.Context, tool *model.CaptainCustomTool) gin.H { + payload := gin.H{ "id": tool.ID, "slug": tool.Slug, "title": tool.Title, @@ -256,11 +279,19 @@ func captainCustomToolPayload(tool *model.CaptainCustomTool) gin.H { "request_template": tool.RequestTemplate, "response_template": tool.ResponseTemplate, "auth_type": tool.AuthType, - "auth_config": rawJSONValue(tool.AuthConfig), "param_schema": rawJSONValue(tool.ParamSchema), "enabled": tool.Enabled, "account_id": tool.AccountID, "created_at": tool.CreatedAt.Unix(), "updated_at": tool.UpdatedAt.Unix(), } + if captainCustomToolShowAuthConfig(c) { + payload["auth_config"] = rawJSONValue(tool.AuthConfig) + } + return payload +} + +func captainCustomToolShowAuthConfig(c *gin.Context) bool { + role, exists := c.Get("role") + return exists && (role == "administrator" || role == "super_admin") } diff --git a/internal/handler/api/v1/captain_custom_tool_test_handler_test.go b/internal/handler/api/v1/captain_custom_tool_test_handler_test.go index c5e6a268..f32e8e44 100644 --- a/internal/handler/api/v1/captain_custom_tool_test_handler_test.go +++ b/internal/handler/api/v1/captain_custom_tool_test_handler_test.go @@ -79,6 +79,12 @@ func (s *CaptainCustomToolTestHandlerTestSuite) SetupSuite() { // 设置路由 s.router = gin.New() + s.router.Use(func(c *gin.Context) { + if role := c.GetHeader("X-Test-Role"); role != "" { + c.Set("role", role) + } + c.Next() + }) accountsGroup := s.router.Group("/api/v1/accounts/:account_id") { captainGroup := accountsGroup.Group("/captain") @@ -134,6 +140,25 @@ func (s *CaptainCustomToolTestHandlerTestSuite) TestTestTool_无效accountID() { assert.Equal(s.T(), http.StatusBadRequest, w.Code) } +func (s *CaptainCustomToolTestHandlerTestSuite) TestTestTool_AgentRoleForbidden() { + body := map[string]interface{}{ + "tool_id": float64(s.tool.ID), + "params": map[string]interface{}{}, + } + jsonBody, _ := json.Marshal(body) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.account.ID), 10)+"/captain/custom_tools/test", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Test-Role", "agent") + s.router.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusForbidden, w.Code) + var resp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(s.T(), "You are not authorized to do this action", resp["error"]) +} + func (s *CaptainCustomToolTestHandlerTestSuite) TestTestTool_缺少tool_id() { body := map[string]interface{}{ "params": map[string]interface{}{},