(ctx context.Context, in *createAgentInput)
| 181 | } |
| 182 | |
| 183 | func (s *Server) handleCreateAgent(ctx context.Context, in *createAgentInput) (*createAgentOutput, error) { |
| 184 | user, err := s.requireAccountUser(ctx) |
| 185 | if err != nil { |
| 186 | return nil, err |
| 187 | } |
| 188 | req := in.Body |
| 189 | email := identity.NormalizeEmail(req.Email) |
| 190 | |
| 191 | if email == "" { |
| 192 | return nil, NewError(http.StatusBadRequest, "invalid_request", "email is required") |
| 193 | } |
| 194 | |
| 195 | // Resolve the DNS domain from the email itself (AG-1/AG-2): there is one |
| 196 | // create path. An email on the deployment's shared domain is a shared-domain |
| 197 | // registration (its local-part is validated as a slug, no ownership check); |
| 198 | // any other domain is a custom-domain agent gated by ownership below. |
| 199 | parts := strings.SplitN(email, "@", 2) |
| 200 | if len(parts) != 2 || parts[0] == "" || parts[1] == "" { |
| 201 | return nil, NewError(http.StatusBadRequest, "invalid_request", "invalid email address") |
| 202 | } |
| 203 | domain := parts[1] |
| 204 | isShared := s.deps.SharedDomain != "" && strings.EqualFold(domain, s.deps.SharedDomain) |
| 205 | if isShared { |
| 206 | domain = s.deps.SharedDomain // normalize to the configured casing |
| 207 | if err := validateSlug(parts[0]); err != nil { |
| 208 | return nil, NewError(http.StatusBadRequest, "invalid_slug", err.Error()) |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | // Custom-domain ownership guard (decision 1): the domain must be |
| 213 | // registered to this user AND verified. This is the load-bearing |
| 214 | // authorization that an agent can only be created on a domain the |
| 215 | // caller controls. |
| 216 | if !isShared { |
| 217 | if s.deps.LookupDomain == nil { |
| 218 | return nil, NewError(http.StatusInternalServerError, "internal_error", "domain lookup unavailable") |
| 219 | } |
| 220 | dom, err := s.deps.LookupDomain(ctx, domain, user.ID) |
| 221 | if err != nil { |
| 222 | return nil, NewError(http.StatusBadRequest, "domain_not_registered", "register and verify your domain first") |
| 223 | } |
| 224 | if !dom.Verified { |
| 225 | return nil, NewError(http.StatusBadRequest, "domain_not_verified", "verify your domain first") |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | // Per-user agent cap (after auth + domain checks, so a 402 means |
| 230 | // "valid request, out of capacity" — never masks a 400/401). |
| 231 | if s.deps.EnforceAgentCreate != nil { |
| 232 | if err := s.deps.EnforceAgentCreate(ctx, user.ID); err != nil { |
| 233 | if env, ok := limitEnvelope(err); ok { |
| 234 | return nil, env |
| 235 | } |
| 236 | return nil, NewError(http.StatusInternalServerError, "internal_error", "limits check failed") |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | if s.deps.CreateAgent == nil { |
nothing calls this directly
no test coverage detected