From be9bbf4f8352d7bcfa089957dda06690d158b581 Mon Sep 17 00:00:00 2001 From: oabrivard Date: Tue, 10 Mar 2026 08:13:35 +0100 Subject: [PATCH] docs(release): add protocol runbooks and rollout checklist --- README.md | 6 +++ docs/a2a.md | 107 +++++++++++++++++++++++++++++++++++++++++++++ docs/mcp.md | 102 +++++++++++++++++++++++++++++++++++++++++++ docs/security.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 325 insertions(+) create mode 100644 docs/a2a.md create mode 100644 docs/mcp.md create mode 100644 docs/security.md diff --git a/README.md b/README.md index b8d3600..75d48bf 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,12 @@ curl -X POST "http://127.0.0.1:8000/unsubscribe/auto-run?max_results=500" \ - Selected lists can be remembered (`remember_selection=true`) for scheduled auto-unsubscribe. - State is saved in `UNSUBSCRIBE_HIL_STATE_FILE`. +## Runbooks + +- A2A protocol runbook: `docs/a2a.md` +- MCP protocol runbook: `docs/mcp.md` +- Security, rotation, and rollout checklist: `docs/security.md` + ## Notes - Gmail "folders" are labels. This agent creates: diff --git a/docs/a2a.md b/docs/a2a.md new file mode 100644 index 0000000..178c795 --- /dev/null +++ b/docs/a2a.md @@ -0,0 +1,107 @@ +# A2A Runbook + +## Scope + +This runbook covers the Agent-to-Agent (A2A) adapter exposed by the REST service in `app.main`. + +## Endpoints + +- Agent Card: `GET /.well-known/agent-card.json` +- RPC endpoint: `POST /a2a/rpc` + +When using Docker Compose: + +- Base URL: `http://127.0.0.1:8000` + +## Protocol contract + +- `protocolVersion` advertised in Agent Card: `1.0` +- Response header on A2A routes: `A2A-Version: 1.0` +- JSON-RPC version: `2.0` + +Implemented methods: + +- `ping` / `health.ping` / `health/ping` +- `SendMessage` (availability only) + +## Authentication + +The A2A adapter uses the same auth backend as REST: + +- `AUTH_MODE=api_key`: `X-API-Key` or `Authorization: Bearer ` +- `AUTH_MODE=jwt`: `Authorization: Bearer ` +- `AUTH_MODE=hybrid`: API key first, then JWT + +Required scope for `SendMessage`: + +- `availability:read` + +## Request shape for SendMessage + +`SendMessage` accepts several input shapes; the request must contain at least: + +- `start` ISO datetime with timezone offset +- `end` ISO datetime with timezone offset + +Accepted locations: + +- `params.start` / `params.end` +- `params.input.start` / `params.input.end` +- `params.arguments.start` / `params.arguments.end` +- JSON embedded in message content + +Optional: + +- `calendar_ids`: array of calendar ids (defaults to `["primary"]`) + +## Smoke tests + +Agent Card: + +```bash +curl http://127.0.0.1:8000/.well-known/agent-card.json +``` + +Availability: + +```bash +curl -X POST "http://127.0.0.1:8000/a2a/rpc" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $AGENT_API_KEY" \ + -d '{ + "jsonrpc":"2.0", + "id":"req-1", + "method":"SendMessage", + "params":{ + "start":"2026-03-10T09:00:00+01:00", + "end":"2026-03-10T10:00:00+01:00", + "calendar_ids":["primary"] + } + }' +``` + +## Error mapping + +- `-32600`: invalid JSON-RPC request +- `-32601`: unknown method +- `-32602`: invalid params (including bad time window) +- `-32001`: unauthorized +- `-32000`: backend/runtime error + +## Troubleshooting + +If you get `-32001`: + +- Verify `AUTH_MODE` +- Verify API key/JWT and scope `availability:read` + +If you get `-32602`: + +- Ensure `start` and `end` include timezone offsets +- Ensure `end > start` + +If you get `-32000` with OAuth file errors: + +- Check `GOOGLE_CLIENT_SECRETS_FILE` path +- Check `GOOGLE_TOKEN_FILE` path + diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..e2245c9 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,102 @@ +# MCP Runbook + +## Scope + +This runbook covers the MCP adapter exposed by `app.mcp_main`. + +## Endpoint + +- Streamable HTTP endpoint: `POST /mcp` (mounted under `app.mcp_main`) + +When using Docker Compose: + +- Base URL: `http://127.0.0.1:8001/mcp` + +## Runtime modes + +Local: + +```bash +uv run uvicorn app.mcp_main:app --host 0.0.0.0 --port 8001 +``` + +Docker Compose service: + +- `personal-agent-mcp` + +## Tool surface + +Always enabled: + +- `check_availability` + +Optional mutation tools (disabled by default): + +- `scan_mailbox` +- `list_unsubscribe_candidates` +- `execute_unsubscribe` + +Enable optional tools with: + +```bash +MCP_ENABLE_MUTATION_TOOLS=true +``` + +## Authorization and scope gates + +MCP tools call the shared auth backend and read auth headers from request context. + +Supported auth headers: + +- `X-API-Key` +- `Authorization: Bearer ...` + +Required scopes: + +- `check_availability`: `availability:read` +- `scan_mailbox`: `mail:scan` +- `list_unsubscribe_candidates`: `unsubscribe:read` +- `execute_unsubscribe`: `unsubscribe:execute` + +## Tool verification + +Verify tool list from Python: + +```bash +uv run python - <<'PY' +import asyncio +from app.mcp.server import mcp + +async def main(): + tools = await mcp.list_tools() + print([t.name for t in tools]) + +asyncio.run(main()) +PY +``` + +Expected output by mode: + +- default: `['check_availability']` +- with `MCP_ENABLE_MUTATION_TOOLS=true`: all four tools + +## Protocol notes + +- The server uses FastMCP Streamable HTTP. +- Basic GET to `/mcp` is not a health endpoint; MCP expects protocol-compliant requests. +- In local development, FastMCP may enforce host/origin checks. If you see `421 Misdirected Request`, verify host/port and reverse-proxy headers. + +## Troubleshooting + +If tools fail with auth errors: + +- Check `AUTH_MODE` and credentials +- Confirm JWT contains required scopes +- For API key mode, verify `AGENT_API_KEY` + +If tool calls fail with Google errors: + +- Verify OAuth file mounts in Docker: + - `GOOGLE_CLIENT_SECRETS_FILE` + - `GOOGLE_TOKEN_FILE` + diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..010a587 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,110 @@ +# Security Runbook + +## Security model + +The service supports three auth modes via `AUTH_MODE`: + +- `api_key`: static shared secret (`AGENT_API_KEY`) +- `jwt`: bearer JWT with scope checks +- `hybrid`: API key accepted first, then JWT fallback + +The same backend is used across: + +- REST API +- A2A adapter +- MCP tools + +## Recommended deployment posture + +External traffic: + +- Use `AUTH_MODE=jwt` +- Require HTTPS at reverse proxy/gateway +- Restrict exposed routes to required protocol endpoints + +Internal traffic: + +- `AUTH_MODE=hybrid` is acceptable during migration +- Prefer mTLS/private network for service-to-service traffic + +## Scope matrix + +- `availability:read`: availability access +- `mail:scan`: inbox scan and triage +- `unsubscribe:read`: candidate discovery +- `unsubscribe:execute`: unsubscribe execution +- `unsubscribe:digest`: digest scan and send + +## Secret and token handling + +Never commit secrets: + +- `.env` +- `token.json` +- Google OAuth client secret files + +Always persist and back up: + +- `token.json` +- `data/sent_unsubscribe_links.json` +- `data/unsubscribed_methods.json` + +## Key and token rotation + +### API key rotation (api_key/hybrid) + +1. Generate new strong key. +2. Update environment (`AGENT_API_KEY`) in deployment. +3. Restart services. +4. Update all clients. +5. Remove old key from all stores. + +### JWT secret rotation (jwt/hybrid) + +1. Generate new signing secret. +2. Roll out issuer/signing config first. +3. Update server `AUTH_JWT_SECRET`. +4. Restart services. +5. Force token refresh for clients. + +## Incident response checklist + +If credential leak is suspected: + +1. Revoke compromised key/secret immediately. +2. Rotate API key and JWT secret. +3. Invalidate active tokens (issuer-side). +4. Review logs for unusual scans/unsubscribe operations. +5. Disable mutation MCP tools (`MCP_ENABLE_MUTATION_TOOLS=false`) until investigation completes. +6. Re-enable features after containment and verification. + +## Release rollout checklist + +Preflight: + +1. `uv run pytest -q` +2. `uv run python -c "import app.main, app.mcp_main; print('import_ok')"` +3. `docker compose config --services` + +Canary: + +1. Deploy to one node/environment. +2. Validate: + - `GET /health` + - `GET /.well-known/agent-card.json` + - A2A `SendMessage` + - MCP tool listing +3. Monitor errors for 30-60 minutes. + +Full rollout: + +1. Deploy all nodes. +2. Re-run smoke checks. +3. Confirm scheduler jobs continue as expected. + +Rollback: + +1. Redeploy previous image/tag. +2. Verify health and protocol smoke checks. +3. Keep state files (`data/*.json`) unchanged during rollback. +