feat(capability-requests): add cross-domain capability catalog and request routing

Introduces a capability catalog (CUST-WP-0022) so domains can advertise what
they provide and agents can request capabilities from other domains with
auto-routing, lifecycle tracking, and task-unblocking on completion.

- New models: CapabilityCatalog, CapabilityRequest with full lifecycle
  (requested → accepted → in_progress → ready_for_review → completed/rejected/withdrawn)
- Migration i6d7e8f9a0b1: capability_catalog + capability_requests tables
- Router /capability-catalog and /capability-requests with accept/status endpoints
- 7 new MCP tools: register_capability, list_capabilities, request_capability,
  accept_capability_request, update_capability_request_status,
  list_capability_requests, get_capability_request
- StateSummary gains open_capability_requests count
- Dashboard: capability-requests.md page + docs/capabilities.md + docs/scope.md
- SCOPE.md: three seed capabilities documented (MCP registration, state tracking, SBOM)
- scope.template: Provided Capabilities section with example block
- scripts/ingest_capabilities.py + make ingest-capabilities[/-all] targets

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 21:07:50 +01:00
parent 7bf3cf583a
commit d45234531b
18 changed files with 2105 additions and 1 deletions

View File

@@ -1729,6 +1729,173 @@ def reply_to_message(message_id: str, from_agent: str, body: str) -> str:
return json.dumps(_post(f"/messages/{message_id}/reply", {"from_agent": from_agent, "body": body}), indent=2)
# ---------------------------------------------------------------------------
# Capability Catalog & Requests
# ---------------------------------------------------------------------------
@mcp.tool()
def register_capability(
domain: str,
capability_type: str,
title: str,
description: str | None = None,
keywords: list[str] | None = None,
) -> str:
"""Register a capability that a domain can provide. Used for routing requests.
Args:
domain: Domain slug (e.g. 'railiance', 'markitect')
capability_type: Category (e.g. 'infrastructure', 'api', 'data', 'security', 'documentation')
title: Short title for this capability
description: Longer description (optional)
keywords: List of keywords for routing (e.g. ['cluster', 'k8s', 'privacy'])
"""
entry = _post("/capability-catalog", {
"domain": domain,
"capability_type": capability_type,
"title": title,
"description": description,
"keywords": keywords or [],
})
return json.dumps(entry, indent=2)
@mcp.tool()
def list_capabilities(
domain: str | None = None,
capability_type: str | None = None,
) -> str:
"""Browse the capability catalog — what domains can provide.
Args:
domain: Filter by domain slug (optional)
capability_type: Filter by type (optional)
"""
return json.dumps(_get("/capability-catalog", {
"domain": domain,
"capability_type": capability_type,
}), indent=2)
@mcp.tool()
def request_capability(
title: str,
description: str,
capability_type: str,
requesting_agent: str,
requesting_domain: str,
requesting_workstream_id: str | None = None,
priority: str = "medium",
blocking_task_id: str | None = None,
) -> str:
"""Request a capability from another domain. Auto-routes to the responsible
domain via the capability catalog. If no unique match, broadcasts to all.
Args:
title: Short title (e.g. 'Privacy idea instance on cluster')
description: Detailed description of what you need
capability_type: Category (e.g. 'infrastructure', 'api', 'data', 'security')
requesting_agent: Your agent identifier (e.g. 'net-kingdom-worker')
requesting_domain: Your domain slug (e.g. 'custodian')
requesting_workstream_id: UUID of your workstream (optional)
priority: low | medium | high | critical (default: medium)
blocking_task_id: UUID of the task blocked until this is fulfilled (optional)
"""
req = _post("/capability-requests", {
"title": title,
"description": description,
"capability_type": capability_type,
"requesting_agent": requesting_agent,
"requesting_domain": requesting_domain,
"requesting_workstream_id": requesting_workstream_id,
"priority": priority,
"blocking_task_id": blocking_task_id,
})
_post("/progress", {
"event_type": "capability_requested",
"summary": f"Capability requested: {title} ({capability_type})",
"author": requesting_agent,
"detail": {
"capability_request_id": req.get("id"),
"capability_type": capability_type,
"routed_to": req.get("fulfilling_domain_slug"),
},
})
return json.dumps(req, indent=2)
@mcp.tool()
def accept_capability_request(
request_id: str,
fulfilling_agent: str,
fulfilling_workstream_id: str | None = None,
) -> str:
"""Accept a capability request. Assigns yourself as the fulfilling agent.
Args:
request_id: UUID of the capability request
fulfilling_agent: Your agent identifier (e.g. 'railiance-worker')
fulfilling_workstream_id: UUID of your workstream for this work (optional)
"""
result = _post(f"/capability-requests/{request_id}/accept", {
"fulfilling_agent": fulfilling_agent,
"fulfilling_workstream_id": fulfilling_workstream_id,
})
return json.dumps(result, indent=2)
@mcp.tool()
def update_capability_request_status(
request_id: str,
status: str,
note: str | None = None,
) -> str:
"""Advance a capability request through its lifecycle.
On 'completed': auto-unblocks the blocking task if one was set.
Args:
request_id: UUID of the capability request
status: in_progress | ready_for_review | completed | rejected | withdrawn
note: Optional note (required for rejection, recommended for completion)
"""
result = _patch(f"/capability-requests/{request_id}/status", {
"status": status,
"note": note,
})
return json.dumps(result, indent=2)
@mcp.tool()
def list_capability_requests(
domain: str | None = None,
status: str | None = None,
capability_type: str | None = None,
) -> str:
"""List capability requests with optional filters.
Args:
domain: Filter by requesting OR fulfilling domain slug
status: Filter by status (requested/accepted/in_progress/ready_for_review/completed/rejected/withdrawn)
capability_type: Filter by capability type
"""
return json.dumps(_get("/capability-requests", {
"domain": domain,
"status": status,
"capability_type": capability_type,
}), indent=2)
@mcp.tool()
def get_capability_request(request_id: str) -> str:
"""Get a single capability request by ID.
Args:
request_id: UUID of the capability request
"""
return json.dumps(_get(f"/capability-requests/{request_id}"), indent=2)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------