executeRequest builds + serves + packages a single HTTP request against the wrapped handler. Pulled out of Dispatch so the special-case methods (dispatchItemUpdate, future RMW commands) can reuse the same auth-context + recorder + response-shaping path. Scope enforcement lives in buildAuthedRequest
( ctx context.Context, cmdKey string, user *models.User, method, urlPath string, body []byte, )
| 443 | // through this method. Codex review #369 round 2 caught the leak that |
| 444 | // motivated centralizing the check. |
| 445 | func (d *HTTPHandlerDispatcher) executeRequest( |
| 446 | ctx context.Context, |
| 447 | cmdKey string, |
| 448 | user *models.User, |
| 449 | method, urlPath string, |
| 450 | body []byte, |
| 451 | ) (*mcp.CallToolResult, error) { |
| 452 | req, err := d.buildAuthedRequest(ctx, method, urlPath, body, user) |
| 453 | if err != nil { |
| 454 | // Most build-request failures are scope-rejection from |
| 455 | // buildAuthedRequest's TokenScopeAllows check (PATCH on a |
| 456 | // read-only token, etc.). Surface as permission_denied so |
| 457 | // agents see the same code as a backend 403, not a |
| 458 | // generic server_error. |
| 459 | if strings.HasPrefix(err.Error(), "permission_denied:") { |
| 460 | return NewErrorResult(ErrorPayload{ |
| 461 | Code: ErrPermissionDenied, |
| 462 | Message: fmt.Sprintf("%s: %s", cmdKey, err.Error()), |
| 463 | Hint: "Token scope does not permit this operation. Re-issue with the required scopes (read for GET; write for POST/PATCH; admin for workspace settings).", |
| 464 | }), nil |
| 465 | } |
| 466 | // PLAN-1933 DR-4: the RequireVerifiedEmail gate rejected this |
| 467 | // write because the cloud user hasn't verified their email. |
| 468 | // Surface as permission_denied (the closest closed-set code) so |
| 469 | // agents branch consistently with a backend 403. |
| 470 | if strings.HasPrefix(err.Error(), errEmailNotVerifiedPrefix+":") { |
| 471 | return NewErrorResult(ErrorPayload{ |
| 472 | Code: ErrPermissionDenied, |
| 473 | Message: fmt.Sprintf("%s: %s", cmdKey, err.Error()), |
| 474 | Hint: "Verify your email address (check your inbox for the verification link) before creating or editing content.", |
| 475 | }), nil |
| 476 | } |
| 477 | return dispatcherErrorResult(cmdKey, "build request", err), nil |
| 478 | } |
| 479 | |
| 480 | rec := httptest.NewRecorder() |
| 481 | d.Handler.ServeHTTP(rec, req) |
| 482 | // Use req.Context() (NOT the outer ctx) so the workspace lister |
| 483 | // sees everything buildHTTPRequest + d.Apply attached: |
| 484 | // WithCurrentUser, WithAPITokenAuth, and any TokenAllowedWorkspaces |
| 485 | // the dispatcher's Apply hook layered on. Tests that drive |
| 486 | // executeRequest with context.Background() + a UserResolver get |
| 487 | // the user via req.Context(), not via the outer ctx — without |
| 488 | // this fix the lister would silently return empty hints in |
| 489 | // those test paths and (more critically) in any production |
| 490 | // dispatcher that attaches token state via Apply rather than |
| 491 | // inheriting from the inbound request's ctx. Codex review #379 |
| 492 | // round 1. |
| 493 | return packageHTTPResponse(req.Context(), cmdKey, rec.Result(), d.Lister) |
| 494 | } |
| 495 | |
| 496 | // buildAuthedRequest constructs an in-process HTTP request against |
| 497 | // the wrapped handler with the user attached via WithCurrentUser AND |