memory.Held and memory.Forget were written the day the store was and had no route, so the honest answer to "show me what you hold about this candidate" was "a developer runs a query". That is not a compliance posture; it is a promise with no mechanism, and a subject access request has a statutory clock. GET /api/v1/memories?subject=candidate&id=… what is held DELETE /api/v1/memories?subject=candidate&id=… erase it Each memory comes back with its author, so "an agent inferred this" and "a recruiter wrote this" stay distinguishable — a response that flattened them would be misleading in the one place it matters most. THE AUTHORISATION IS THE WHOLE DESIGN, AND THE FIRST VERSION WAS WRONG. Gating a read of candidate memories on `list` of job-applications looked right and was not: a talent may list applications because every other read path scopes them to their OWN rows, and this route has no row scoping — so the check passed and the response would have carried the whole organisation's memories about everybody. A test caught it before it shipped. A personal memory now requires `delete` on the record it concerns, for reading as much as for erasing. Deleting somebody's application is an administrative capability and nothing scopes it to self, which makes it the honest proxy for "may act on other people's records here". No new permission, and it cannot drift from the record's own policy. Workspace facts have no personal subject and stay at `list`. Erasing workspace memories refuses without an explicit id: "erase everything" is a plausible thing to want and a catastrophic thing to do by a mistyped query string, so it is not reachable by omission. An erasure is logged at Info with who asked and when — the row itself is redacted, so the log is the durable record that it happened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.8 KiB
3.8 KiB