feat(tpsc): Third-Party Services Catalog (CUST-WP-0023)

Introduces TPSC for tracking external service dependencies with GDPR
compliance maturity (CNIL/IAPP CMMI scale), pricing model, ToS, and
data retention information across all repos.

Primary data:
- canon/tpsc/{openai,anthropic,gemini,openrouter}-api.yaml — service definitions
- tpsc.yaml in each repo (llm-connect seeded with 4 services)

State-hub additions:
- Migration j7e8f9a0b1c2: tpsc_catalog + tpsc_snapshots + tpsc_entries
- api/models/tpsc.py, api/schemas/tpsc.py, api/routers/tpsc.py
- /tpsc/catalog/, /tpsc/ingest/, /tpsc/snapshots/, /tpsc/report/gdpr endpoints
- 4 MCP tools: register_service, list_services, ingest_tpsc_tool, get_gdpr_report
- scripts/ingest_tpsc.py + make ingest-tpsc[/-all] targets
- Dashboard: tpsc.md page + docs/tpsc.md

GDPR maturity scale: unknown | non_compliant | initial | developing | defined | managed | certified
Warnings triggered at: unknown, non_compliant, initial

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 00:15:26 +01:00
parent 4e28cab297
commit 60beb1ff35
14 changed files with 1126 additions and 1 deletions

View File

@@ -1911,6 +1911,140 @@ def get_capability_request(request_id: str) -> str:
return json.dumps(_get(f"/capability-requests/{request_id}"), indent=2)
# ---------------------------------------------------------------------------
# Third-Party Services Catalog (TPSC)
# ---------------------------------------------------------------------------
@mcp.tool()
def register_service(
slug: str,
name: str,
provider: str | None = None,
category: str | None = None,
pricing_model: str = "unknown",
gdpr_maturity: str = "unknown",
gdpr_notes: str | None = None,
dpa_available: bool = False,
tos_url: str | None = None,
privacy_policy_url: str | None = None,
data_processing_regions: list[str] | None = None,
data_retention_notes: str | None = None,
website_url: str | None = None,
) -> str:
"""Register or update a service in the Third-Party Services Catalog (TPSC).
GDPR maturity scale (CNIL/IAPP CMMI-aligned):
unknown | non_compliant | initial | developing | defined | managed | certified
Pricing model: free | paid | freemium | usage_based | unknown
Args:
slug: Unique identifier (e.g. 'openai-api', 'stripe')
name: Human-readable service name
provider: Company/organisation name
category: Category (e.g. 'llm_inference', 'storage', 'payments', 'search')
pricing_model: free | paid | freemium | usage_based | unknown
gdpr_maturity: GDPR compliance maturity level (see scale above)
gdpr_notes: Free-text GDPR notes (DPA details, transfer mechanisms, etc.)
dpa_available: Whether a Data Processing Agreement is available
tos_url: Terms of Service URL
privacy_policy_url: Privacy Policy URL
data_processing_regions: List of regions where data is processed (e.g. ['us', 'eu'])
data_retention_notes: Data retention policy summary
website_url: Service website URL
"""
return json.dumps(_post("/tpsc/catalog", {
"slug": slug,
"name": name,
"provider": provider,
"category": category,
"website_url": website_url,
"pricing_model": pricing_model,
"gdpr_maturity": gdpr_maturity,
"gdpr_notes": gdpr_notes,
"dpa_available": dpa_available,
"tos_url": tos_url,
"privacy_policy_url": privacy_policy_url,
"data_processing_regions": data_processing_regions or [],
"data_retention_notes": data_retention_notes,
}), indent=2)
@mcp.tool()
def list_services(
gdpr_maturity: str | None = None,
category: str | None = None,
pricing_model: str | None = None,
) -> str:
"""Browse the Third-Party Services Catalog (TPSC).
Returns services with their GDPR maturity level and gdpr_warning flag
(True when maturity is unknown, non_compliant, or initial — may limit
use in corporate/GDPR-regulated environments).
Args:
gdpr_maturity: Filter by maturity level (unknown/non_compliant/initial/developing/defined/managed/certified)
category: Filter by category (e.g. 'llm_inference', 'storage')
pricing_model: Filter by pricing model (free/paid/freemium/usage_based/unknown)
"""
return json.dumps(_get("/tpsc/catalog", {
"gdpr_maturity": gdpr_maturity,
"category": category,
"pricing_model": pricing_model,
}), indent=2)
@mcp.tool()
def ingest_tpsc_tool(repo_slug: str) -> str:
"""Ingest tpsc.yaml service dependency declarations for a repo.
Reads <repo_root>/tpsc.yaml, resolves service slugs against the catalog,
and creates a new TPSC snapshot. The repo path is resolved the same way
as the SBOM ingest tool (host_paths → local_path with existence check).
Args:
repo_slug: Registered repo slug (e.g. 'llm-connect', 'markitect-project')
"""
import socket as _socket
import subprocess
repo = _get(f"/repos/{repo_slug}")
if isinstance(repo, dict) and repo.get("error"):
return f"Repo '{repo_slug}' not found: {repo['error']}"
repo_root = _resolve_repo_path(repo)
if not repo_root:
hostname = _socket.gethostname()
return (
f"⚠ No accessible path found for repo '{repo_slug}' on host '{hostname}'.\n"
f"Register with: update_repo_path('{repo_slug}', '/path/to/repo')"
)
script = Path(__file__).parent.parent / "scripts" / "ingest_tpsc.py"
result = subprocess.run(
["uv", "run", "python", str(script), "--repo", repo_slug],
capture_output=True, text=True,
cwd=str(Path(__file__).parent.parent),
)
output = result.stdout + result.stderr
if result.returncode != 0:
return f"ingest_tpsc failed (exit {result.returncode}):\n{output}"
return output.strip()
@mcp.tool()
def get_gdpr_report() -> str:
"""Get an aggregated GDPR compliance report across all repos' latest TPSC snapshots.
Returns a warning summary for services with gdpr_maturity in:
unknown | non_compliant | initial
These may limit usability in GDPR-regulated / corporate environments.
Services at 'developing' or above have at least a DPA available.
"""
return json.dumps(_get("/tpsc/report/gdpr"), indent=2)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------