BuildAgentBootstrap assembles the bootstrap blob from store queries. This is the single canonical code path; the HTTP handler, the MCP resource handler, and the MCP `pad_set_workspace` embed all call this. r is the live request — used for the dashboard sub-build AND to resolve the calling principal
(workspaceID string, user *models.User, r *http.Request)
| 421 | // safe ONLY for callers that have already verified full-member access |
| 422 | // out-of-band. Production HTTP/MCP paths MUST pass the live request. |
| 423 | func (s *Server) BuildAgentBootstrap(workspaceID string, user *models.User, r *http.Request) (*AgentBootstrap, error) { |
| 424 | ws, err := s.store.GetWorkspaceByID(workspaceID) |
| 425 | if err != nil { |
| 426 | return nil, err |
| 427 | } |
| 428 | |
| 429 | out := &AgentBootstrap{ |
| 430 | Workspace: AgentBootstrapWorkspace{ |
| 431 | ID: ws.ID, |
| 432 | Slug: ws.Slug, |
| 433 | Name: ws.Name, |
| 434 | Description: ws.Description, |
| 435 | }, |
| 436 | } |
| 437 | if user != nil { |
| 438 | out.User = AgentBootstrapUser{ |
| 439 | ID: user.ID, |
| 440 | Name: user.Name, |
| 441 | Email: user.Email, |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | // Resolve visibility once so collections/conventions/playbooks/roles |
| 446 | // all project the same authorized view. nil visibleIDs means "no |
| 447 | // restriction" — a full workspace member (or a nil-r caller that |
| 448 | // has already verified access out-of-band). For guests with |
| 449 | // item-level grants, we also need ItemIDs filtering so a grant to |
| 450 | // one specific playbook doesn't leak the whole collection. |
| 451 | var visibleIDs []string |
| 452 | var grantedItemIDs []string |
| 453 | var fullCollIDs []string |
| 454 | if r != nil { |
| 455 | visibleIDs, err = s.visibleCollectionIDs(r, workspaceID) |
| 456 | if err != nil { |
| 457 | return nil, err |
| 458 | } |
| 459 | fullCollIDs, grantedItemIDs, err = s.guestResourceFilter(r, workspaceID) |
| 460 | if err != nil { |
| 461 | return nil, err |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | // Collections — load and apply visibility filtering. We hold these |
| 466 | // in their full models.Collection shape through the role/count |
| 467 | // recompute below (which keys lookups by Collection.ID), then |
| 468 | // project to BootstrapCollection at the end of this section. The |
| 469 | // projection drops id/workspace_id/timestamps/settings and parses |
| 470 | // the schema string into a nested object — see BootstrapCollection |
| 471 | // godoc + PLAN-1410 / TASK-1412. |
| 472 | collections, err := s.store.ListCollections(workspaceID) |
| 473 | if err != nil { |
| 474 | return nil, err |
| 475 | } |
| 476 | if visibleIDs != nil { |
| 477 | filtered := make([]models.Collection, 0, len(collections)) |
| 478 | for _, c := range collections { |
| 479 | if isCollectionVisible(c.ID, visibleIDs) { |
| 480 | filtered = append(filtered, c) |
no test coverage detected