Files
pad/internal/server
xarmian 24f0445efa feat(bootstrap): single-roundtrip /pad context-load endpoint (TASK-1379) (#518)
* feat(bootstrap): single-roundtrip /pad context-load endpoint (TASK-1379)

Implements the agent bootstrap surface for PLAN-1377. One HTTP call
replaces the four separate /pad context-loading invocations (workspace
+ collections + conventions + roles + playbooks) the skill used to
make, cutting ~200-400ms of startup latency on every /pad command.

Wire shape:

- GET /api/v1/workspaces/{ws}/agent/bootstrap returns AgentBootstrap:
  workspace { slug, name, id }, user { name, email, id },
  collections [...], conventions [...always-on, status=active],
  roles [...], playbooks [metadata-only — no bodies], dashboard {...},
  recent_activity [... 24h].
- pad bootstrap [--format json|markdown] CLI wrapper. JSON is the
  canonical wire format that the /pad skill consumes; markdown is a
  human-readable summary for quick terminal inspection.

Implementation notes:

- Single source of truth: Server.BuildAgentBootstrap. The HTTP handler
  is a thin wrapper, and TASK-1380 will reuse it from three MCP
  surfaces (resource + set_workspace embed + pad_meta tool action).
- Dashboard reuse: handleGetDashboard's body extracted into
  buildDashboardResponse(workspaceID, r) returning (*DashboardResponse,
  error). The HTTP handler is now a thin wrapper; bootstrap calls the
  builder directly to embed dashboard data without a second roundtrip.
- Playbook bodies are deliberately NOT included — metadata only
  (~80 bytes/entry vs 5-10KB) so the bootstrap stays small for
  workspaces with many playbooks. Full bodies load on invocation.
- Convention bodies ARE included for the always-on/active subset (must
  be agent-known up front); trigger-specific conventions stay
  load-on-demand.
- Empty slices serialize as [] not null so the agent doesn't need
  defensive nil checks.

Tests:

- TestBootstrapEmptyWorkspace, TestBootstrapEmptyArraysNotNull,
  TestBootstrapIncludesPlaybookMetadata,
  TestPlaybookSummaryPrefersFirstParagraph.

Parent: PLAN-1377.

* fix(bootstrap): respect collection visibility + guest grants (TASK-1379)

Codex round 1: BuildAgentBootstrap was bypassing visibility filters,
so a guest admitted by RequireWorkspaceAccess could read collections,
conventions, and playbook metadata they don't have access to.

Now mirrors handleListCollections + handleGetDashboard:

1. Resolve visibleCollectionIDs(r, workspaceID) once.
2. Filter the Collections array through isCollectionVisible.
3. Gate the conventions + playbooks sub-queries on whether the caller
   can see those collections at all (presence in the filtered slice
   implies visibility).
4. Empty slices for inaccessible sub-resources, so the wire shape
   stays consistent — no missing keys to confuse the agent skill.
5. Pass-r=nil callers (future MCP in-process dispatchers) keep the
   'full visibility' shortcut, but the doc comment now spells out that
   those callers MUST verify access out-of-band.

Parent: PLAN-1377.

* fix(bootstrap): apply guest item-level grants + recompute role counts (TASK-1379)

Codex round 2:

P1 — Item-level guest grants now flow into the convention + playbook
sub-queries. visibleCollectionIDs alone admits the conventions /
playbooks collection if a guest has ANY item grant inside it; without
ItemIDs filtering, those queries then return the whole always-on
convention body set or every playbook's metadata. Now mirrors the
handleListItems shape: resolve (fullCollIDs, grantedItemIDs) via
guestResourceFilter, and pass the (collIDs, itemIDs) tuple through to
collectAlwaysOnConventions + collectPlaybookMetadata so a guest with a
single grant only sees that one item.

P2 — AgentRole.ItemCount is now recomputed from the visible item set
for restricted callers, matching handleListAgentRoles. Without this,
a guest could read role counts computed across all workspace items and
infer hidden activity per role.

Helper signatures updated to accept (collIDs, itemIDs); the doc
comments explain the nil/non-nil semantics so future callers can't
silently regress this.

Parent: PLAN-1377.

* fix(bootstrap): rewrite collection counts from visible set for guests (TASK-1379)

Codex round 3: Collection.item_count + active_item_count are computed
across the whole collection by ListCollections, so a guest with one
item grant in a collection still received the collection in the
filtered list but with hidden counts. Reuse the visible item set
(already computed for role counts) to recompute collection item_count
for restricted callers. active_item_count is set equal to item_count
to avoid a separate done-rules buildup the bootstrap consumers don't
depend on — better a self-consistent number than a leaked one.

Parent: PLAN-1377.
2026-05-12 17:51:05 -04:00
..