feat(mcp): add scoped People rename and cleanup tools
Summary
- add hosted MCP and REST operations to rename a recognized person and dismiss a false-positive person
- require
people.renamefor corrections and an explicit opt-inpeople.cleanupscope for dismissal - keep existing and default API keys backward-compatible without silently granting cleanup access
- preserve dismissed records, aliases, history, and speech data while excluding them from normal People reads
- publish the new routes in the OpenAPI contract, generated desktop/web clients, and MCP documentation
Behavior and safety
Rename
rename_person updates the display name while retaining the prior name in a bounded alias list. It is owner-scoped and returns only the person ID and corrected name; it does not return transcript samples.
Dismiss
dismiss_person is deliberately a soft cleanup operation. It records is_dismissed, dismissed_at, and updated_at, and normal People reads skip the record. The operation is idempotent, preserves the original dismissal timestamp, and maps missing or concurrently deleted records to a not-found response.
This PR does not hard-delete a person, speech samples, audio, or historical data. Portability exports explicitly include dismissed people so cleanup does not remove user data from export.
Authorization
people.readremains the read scope.people.renameis included in the compatibility/default scope set so existing clients retain safe correction behavior.people.cleanupis supported by OAuth and API keys but is never added implicitly to legacy, default, or newly created keys.- API-key creation accepts recognized explicit scopes and rejects unknown scopes.
tools/listhidesdismiss_personunless the caller haspeople.cleanup.- runtime tool dispatch and REST routes independently enforce the required scope.
- REST calls retain the existing fail-closed per-app/per-key MCP rate limit.
API surface
- hosted MCP tools:
rename_person,dismiss_person PATCH /v1/mcp/people/{person_id}/namePOST /v1/mcp/people/{person_id}/dismiss- reviewed route-policy entries for both REST operations
- regenerated app-client OpenAPI, macOS Swift, Windows TypeScript, and web TypeScript bindings
Test evidence
- 216 focused backend tests across People storage, MCP tools/REST, OAuth/API-key scopes, key creation, and dependency boundaries
- targeted GDPR/data-export coverage confirms dismissed and locked user data remains exportable
- tool descriptor,
tools/list, allowed/denied scope, validation, not-found, idempotency, concurrent deletion, transcript-leak, and legacy/default-key regression coverage - public, app-client, and integration OpenAPI freshness checks
- TypeScript, Swift, and Dart generated-client freshness checks
- Python compile/format and diff hygiene checks
- full local repository preflight (46 selected checks)
The local Node 26 run uses NODE_OPTIONS=--no-experimental-webstorage so Vitest/jsdom owns localStorage; this only avoids a Node runtime collision and does not alter product behavior.
Product invariants affected
- INV-MEM-3 — the export path continues to use the universal memory authority; this change only makes People export include soft-dismissed records and does not add a legacy memory read path.
Failure class
Failure-Class: none
This is a new, scoped cleanup capability rather than a change to an existing runtime failure class.
Closes #8369
Line-Count-Exception: backend/database/users.py | 2366 -> 2380 | Add soft-dismissal filtering to the existing People read authority without introducing a second persistence path.