Contract: HTTP API Changes
All routes are Next.js BFF routes under ui/src/app/api/. Existing auth
middleware (requireRbacPermission, requireResourcePermission) is reused.
A1. Datasource sharing — extend existing route
GET /api/rag/kbs/[id]/sharing
- Change: response
owner_team_slugMUST be the real owner from the persisted datasource config (today it is alwaysnull). Addcreator_subject(read-only) for display/audit. - Response:
{
"knowledge_base_id": "string",
"owner_team_slug": "string | null",
"shared_team_slugs": ["string"],
"creator_subject": "string | null"
} - Gate unchanged:
knowledge_base#read,bypassForOrgAdmin: true.
PUT /api/rag/kbs/[id]/sharing
- Change: accepts optional
owner_team_slug(create/first-set only — NOT a transfer; rejected if it would change an existing owner). Persists owner + shared to the datasource config and reconciles via the shared helper. - Request:
{ "team_slugs": ["string"], "owner_team_slug": "string?" } - Gate unchanged:
knowledge_base#admin,bypassForOrgAdmin: true.
A2. Datasource creation — capture owner + creator
The datasource create path (BFF proxy on POST v1/datasource) MUST:
- accept
owner_team_slugfrom the request body (already partially wired), - capture
creator_subject = session.sub, - persist all four mixin fields to the datasource config,
- write the
parent_kbedge and owner/creator tuples on upstream success.
A3. Ownership transfer — new capability
A transfer is expressed as owner_team_slug change on a dedicated path or an
explicit ?transfer=true form of the sharing PUT (implementation choice). It:
- Authorization: caller is current owner-team admin (
can_manage) OR org admin. Otherwise403. - Request:
{ "owner_team_slug": "string", "confirm_not_member": boolean? } - Behavior: reconcile with
previousOwnerTeamSlug = <stored owner>,ownerTeamSlug = <new>,allowOwnerTransfer: true; persist new owner. - Response:
{ "owner_team_slug": "string", "reconcile": { ... } } creatortuple unchanged.
Applies uniformly to agents, datasources, and MCP tools (same helper).
A4. Custom MCP tool — create/update/delete
POST /v1/mcp/custom-tools (via BFF proxy)
- Capture
creator_subject,owner_team_slug,shared_with_teamsfrom body; validate owner-team membership; persist toMCPToolConfig; reconcile on upstream success.
PUT /v1/mcp/custom-tools/[tool_id] (via BFF proxy)
- Read previous owner/shared from config; reconcile diff; persist; owner change only via transfer path (A3).
DELETE /v1/mcp/custom-tools/[tool_id] (via BFF proxy)
- New: after upstream delete succeeds, remove ALL
mcp_tool:<id>grants (owner, shared, creator) so no orphan tuples remain (FR-028).
A5. MCP tool invocation enforcement — new gate
The BFF invocation path that forwards to /v1/mcp/invoke MUST, before
forwarding:
- resolve the target
mcp_tool:<id>(custom tools only), Check(<principal>, can_call, mcp_tool:<id>)where<principal>is the session user (user:<sub>) or, for agent-initiated calls,agent:<id>,- deny with a tool-specific
403if the check fails (FR-029).
Built-in tools and non-custom tool names are out of scope for this gate (no
mcp_tool object exists for them).
A6. Error and status conventions (unchanged)
400invalid id / body (existingINVALID_*codes).401no session / no access token.403failed resource permission (existingFORBIDDEN).- Reconcile failures are logged and surfaced in the response
reconcilefield; they do not 500 the config write (config remains source of truth).