# XML-RPC bridge (`devplacepy/services/xmlrpc/`, `devplacepy/routers/xmlrpc.py`)

This file documents the XML-RPC bridge. Claude Code auto-loads it when a file under `devplacepy/services/xmlrpc/` is read or edited.

## Overview

`xmlrpc.py` (the router, mounted at `/xmlrpc`) reverse-proxies XML-RPC calls to the forking XML-RPC bridge (`services/xmlrpc/`, supervised by `XmlrpcService` on loopback `config.XMLRPC_PORT`), which generates one XML-RPC method per documented REST endpoint from `docs_api.API_GROUPS` (`posts.create`, `feed.list`, ...). One struct of named params per call; auth via in-band `api_key`, `X-API-KEY`/`Bearer` header, or `http://user:pass@host/xmlrpc` Basic. Full introspection + `system.multicall`; REST errors become XML-RPC faults. Exempt from the rate limiter (enforced on the forwarded internal hop).

The whole REST surface is also exposed over XML-RPC at `/xmlrpc`, generated **automatically from `docs_api.API_GROUPS`** so it never drifts: add a documented endpoint and it becomes an XML-RPC method for free.

## Pieces

- **`registry.py`** walks `docs_api.API_GROUPS` and yields one `Method` per endpoint. Method name = the endpoint `id` with `-`->`.` (`posts-create` -> `posts.create`). It carries the HTTP method, path, auth, and the `param` list split by `location` (path/query/form). It also builds the `system.methodHelp` text and `system.methodSignature` from the same data.
- **`bridge.py`** translates ONE XML-RPC call into an internal REST request. The single struct argument is a dict of named params; the bridge substitutes `{name}` path segments, routes the rest to query (GET) or form (write methods), sets auth headers, and calls `config.INTERNAL_BASE_URL` with a **synchronous** `stealth.stealth_sync_client` (the forking server is sync). It returns the decoded JSON; any REST `status >= 400` becomes an `xmlrpc.client.Fault` whose `faultCode` is the HTTP status. A missing required param faults before the call.
- **`server.py`** is `ForkingXMLRPCServer(ForkingMixIn, BridgeDispatcher, SimpleXMLRPCServer)`, `allow_none=True`, with `register_introspection_functions()` + `register_multicall_functions()` and one registered function per generated method. The custom request handler captures auth from the POST into `server.current_auth` (safe because each request runs in its own forked child) and the bridge forwards it: `X-API-KEY` and `Bearer` become an api-key header; a raw `Authorization: Basic` header is **forwarded verbatim** so the REST layer does its normal `username/email:password` login (`utils._user_from_basic`) - this is what makes `xmlrpc.client.ServerProxy("http://user:pass@host/xmlrpc")` work. It also forwards `X-Real-IP`/`X-Forwarded-For` so **rate limiting is attributed to the real caller** on the internal REST hop. Run standalone with `python -m devplacepy.services.xmlrpc.server` or the `devplace-xmlrpc` console script.
- **`XmlrpcService(BaseService)`** (`services/xmlrpc/__init__.py`) supervises the server as a **subprocess** (never fork the asyncio/uvicorn process): `on_enable` spawns, `run_once` health-checks and respawns, `on_disable` terminates. `default_enabled=True`, but like every service only the lock-owner worker runs it, so exactly one process binds the port.
- **`routers/xmlrpc.py`** reverse-proxies `/xmlrpc` (POST = RPC, GET = usage banner) to the forking server on `config.XMLRPC_PORT`, forwarding the auth + real-IP headers. `/xmlrpc` is **exempt from the request rate limiter** in `main.py` (enforcement happens on the forwarded internal hop). nginx forwards `/xmlrpc` in production with longer timeouts.

## Auth

Auth has three equivalent forms: `api_key` inside the param struct (works with a vanilla `ServerProxy`), an `X-API-KEY`/`Bearer` header on the transport, or HTTP Basic via a credentialed URL `http://username:password@host/xmlrpc`. Public endpoints need none. Config: `config.XMLRPC_BIND`/`XMLRPC_PORT` (loopback, `10550`). Documented at `/docs/xmlrpc.html`.
