feat(mcp): add scoped People rename and cleanup tools

Merged
AR
11d ago
+1331-59

Summary

  • add hosted MCP and REST operations to rename a recognized person and dismiss a false-positive person
  • require people.rename for corrections and an explicit opt-in people.cleanup scope 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.read remains the read scope.
  • people.rename is included in the compatibility/default scope set so existing clients retain safe correction behavior.
  • people.cleanup is 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/list hides dismiss_person unless the caller has people.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}/name
  • POST /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.