Skip to content

feat(vault): deletion deny-list store + Mark/Filter RPCs (logical delete SSOT) #73

Description

@jh-lee-cryptolab

TL;DR

Memories can't be hard-deleted (enVector has no per-vector delete, only delete_index). So we delete logically: vault keeps a deny-list of deleted item_ids and serves it. Vault is the single source of truth; clients do the filtering. Vault itself does not filter scores and does not call enVector.

Client side: CryptoLabInc/rune-mcp#15

Where vault sits

flowchart LR
    C[rune-mcp client] -- "MarkDeleted(item_ids)" --> V
    C -- "FilterDeleted(candidates)" --> V
    V[(Vault deny-list<br/>SSOT, per index)] -- "deleted subset" --> C
    C -. "filters out deleted hits" .-> R[recall / capture results]
    V -. "never talks to" .-x E[enVector]
Loading

Why these choices

Decision Why
Logical delete (deny-list) enVector exposes no per-vector delete — hard delete is blocked by dependencies
Key = item_id (uint64) Stable logical id from insert / get_metadata. (shard,row) is physical → moves on compaction (dynamic-IP problem)
SSOT read-through, not client sync Low-QPS interactive tool → per-query check is cheap; strong consistency for free; same RPC becomes the sync source later if ever needed
Vault stores, client filters Keeps vault pure (no enVector client, no score-filtering); respects the fixed metadata-decryption flow

Scope (vault only)

2 new RPCs in vault/proto/vault_service.proto:

rpc MarkDeleted   (MarkDeletedRequest)   returns (MarkDeletedResponse);   // write: union item_ids into deny-list
rpc FilterDeleted (FilterDeletedRequest) returns (FilterDeletedResponse); // read: given candidates, return deleted subset

Both carry token + index_name + repeated uint64 item_ids. MarkDeleted returns {version, count}; FilterDeleted returns {deleted_item_ids, version}. Candidate-scoped reads → cost independent of total deny-list size. version (monotonic) lets a future client cache/sync.

Store (vault/internal/...): map[index] -> {set, version}, file-backed with debounce — reuse the tokens/store.go pattern.

Handlers (grpc.go): token-validate → scope-check (mark_deleted / read scope) → audit emit(...), same conventions as existing RPCs.

Checklist

  • MarkDeleted / FilterDeleted (auth + scope + audit)
  • Deny-list persists across restart; idempotent marks; monotonic version
  • Role scope + store_test.go update
  • grpc_test.go unit tests (CLAUDE.md)
  • mise run check green

Open questions

  • ⚠️ Confirm enVector keeps item_id stable across compaction/re-index — the whole design rests on this
  • Storage: YAML (default) vs embedded DB if volume grows
  • filter_deleted own scope vs reuse decrypt_scores
  • Phase-2 ListDeleted(since_version) sync source — defer until needed

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions