Repository navigation
feat(tokens): add orgs_only tenancy so a token can exclude its owner's personal memories - #729
Conversation
…_write Migration 00116 lets org_access be 'orgs_only': lorekit_api_token_org_allowed returns false for a personal row under it, so every tenancy-aware RPC hides the owner's personal memories from such a key without a signature change. memory_write refuses a personal destination (LK002 key_personal_denied) instead of falling back to personal. set_scoping requires >= 1 org, like 'selected'.
tenant-scope (both copies) adds no personal branch for orgs_only and matches nothing when no org is visible; unknown org_access now fails closed to no access instead of 'personal'. Owner-row tools (purge, quota, protect, policy.*, groom.*) are refused for orgs_only on MCP (-32003) and REST (403), and by-id REST writes are narrowed to the key's orgs. Schemas accept orgs_only.
…ore by key memory_delete (00071) and restore_memory (00072) matched own-row and member-org rows without the tenancy predicate, so an orgs_only key could archive, delete or restore its owner's personal memories by (scope, key). 00116 re-creates both with an orgs_only-only clause; other tenancies keep the 00071/00072 WHERE.
read-activity, read-ranking, utility, usage and usage-runs roll up the owner's own rows with no tenancy parameter, so they listed personal keys to an orgs_only token. They join OWNER_ROW_TOOLS and are refused (-32003 / 403).
|
The dashboard preview for this PR — redeployed on each push that changes the web app.
Preview always points at this PR's latest commit; Deployment is the immutable build of |
🟡 1 non-blocking findingorgs_only closes the personal branch on every memory read, write, delete and restore path, and the delta fixes all three earlier findings. The org.* surface still ignores the key's tenancy. Warnings: Body omits the orgs_only citation skip, and says by-id writes are narrowed; they match nothing (404)
Review details
Memories used
Nothing to report — 0 files skipped.
|
Regenerated by the Update visual baselines workflow for PR #729.
|
✅ Update visual baselines: committed 12 updated baseline(s) as ℹ️ This commit was pushed with |
…is refused key_personal_denied reused LK002, so MCP said "ask an org admin to change your role" and REST said "insufficient permissions for this org action" — neither fixes it. Both now name the orgs_only limit and point at `org`.
memory.write's description, mcp-tools.md and org-sharing.md now say an orgs_only key's personal write is refused rather than saved personal; the -32003 row lists the orgs_only refusals; by-id writes are documented as matching nothing (404) for orgs_only; decisions.md names memory_write.
…fresh The whole-suite refresh also rewrote ten unrelated components' screenshots (rendering drift on main). Restore them so this PR changes only the panel it actually changed.
lorekit_record_memory_citations (00107) only credits the owner's personal lessons and takes no tenancy, so an org write from an orgs_only key still raised cited_count on personal rows. Both write surfaces now skip it for a key that cannot reach personal rows.
remote.mdx listed three tenancies and said personal memories stay reachable under all of them; the llms template said bound-scope writes are never rejected. mcp-tools.md now says key_personal_denied is an in-band isError result, not -32003.
org.list / GET /orgs return every org the owner belongs to and the member and invite lists expose handles and invitee emails, none of it filtered by the key's org_ids. orgs_only is the tenancy meant for handed-out tokens, so org.* and member.* are refused for it (-32003 / 403).
Every API-token tenancy could reach the owner's personal memories, so a token you hand out exposes everything personal in its scopes. One example is a public
lk_ro_token bundled in a tool so its users can read one org's curated lore, which is the reaper-mcp case.selectednarrows orgs but never personal rows.This adds
org_access = 'orgs_only'. It works likeselected(at least one org, intersected with current membership), but the token reaches none of its owner's personal rows on any MCP or REST path.lorekit_api_token_org_allowed(…, org_id = null)is now false fororgs_only. Every tenancy-aware RPC already calls it per row, so no signatures change. On the edge,tenant-scope.ts(both copies) adds no personal branch, and the "no visible orgs" fallback now matches nothing.memory_writerefuses a personal destination withLK002 key_personal_deniedinstead of falling back to personal. Delete, archive and restore by scope+key (memory_delete/restore_memory, re-created from 00071/00072) get a clause that applies only toorgs_only. By-id REST writes only ever match the caller's own (personal) rows, so fororgs_onlythey match nothing (404). The write'scitedrefs are not recorded, because citations only ever credit personal lessons.-32003, REST403): operations that only act on the owner's own rows and have no org-scoped form. These are purge, quota, protect,policy.*,groom.*, the owner analytics (read-activity, read-ranking, utility, usage), and all org and membership management (org.*,member.*). Those ignoreorg_idsand would list every org, member handle and invitee email.normalizeKeyRestrictionused to map an unknownorg_accesstopersonal. It now maps it to no access, so an older reader can never widen a newer value.docs/api-tokens.mdand adocs/decisions.mdentry.all,personalandselectedbehave exactly as before. The SQL assertions in §116 cover both the new tenancy and the unchanged ones.Rollout: apply migration 00116 and deploy the edge together. No key can carry
orgs_onlyuntil the CHECK admits it.Out of scope, already broken on
main:selectedandpersonalkeys can still archive member-org rows outside their allowlist through delete/restore by scope+key.Verified locally:
migrations.test.sqlpasses on PGlite (120 sections). Removing a guard fails its assertion.🧪 UI verification spec — run against the preview deployment
Target: preview
Format: intent
Spec 1: Create a token limited to selected orgs, with no personal memories
Changed: the tenancy choice "Selected orgs only (no personal memories)" in the New token form's Scoping panel
Start: /settings/api-keys
Steps:
Expected:
aria-pressed="true").Hints:
aria-pressed; the picker is labelled "Organisations".Spec 2: Out of bounds — switching away drops the picked orgs
Changed: the same Scoping panel
Start: /settings/api-keys
Steps:
Expected: