feat(chat): service-level sensitive gating + local sensitive marks

Wires the sensitive-conversation plumbing into ChatService + Settings:
- Settings gains a persisted local sensitive set (isChatSensitive /
  setChatSensitive / sensitiveChatConversations), mirroring the blocked set, for
  INSTANT effect the moment the user marks a conversation (before the on-chain
  signal is harvested back).
- ChatService::isConversationSensitive() OR-combines that local mark with the
  on-chain reduction, and setPersistence() wires it as the DB's per-row key
  selector (sensitive rows -> vault key).
- ingest() withholds a sensitive conversation's messages while the vault key is
  absent (fail-closed: not shown, not persisted); they re-import + persist under
  the vault key once it is unlocked and a scan runs. loadFromDatabase() already
  withholds via the DB's two-key try.
Service-tested end to end against a real ChatDatabase (ingest + reload).

Phase B, step 4 (service gating). App vault lifecycle + UI next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 17:42:31 -05:00
co-authored by Claude Opus 4.8
parent 6b9bda3ee4
commit fe3d2aa935
5 changed files with 124 additions and 1 deletions
+13 -1
View File
@@ -53,7 +53,8 @@ public:
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
void setPersistence(ChatDatabase* db) { db_ = db; }
// Also wires the DB's per-conversation sensitivity predicate to isConversationSensitive().
void setPersistence(ChatDatabase* db);
// Load previously-persisted messages (already decrypted at ingest, re-encrypted at rest under
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
void loadFromDatabase();
@@ -89,6 +90,16 @@ public:
// Conversations whose latest on-chain control op is "sensitive", each paired with its control label.
std::vector<std::pair<std::string, std::string>> onChainSensitiveConversations() const;
// A local "sensitive" predicate (typically Settings::isChatSensitive) giving INSTANT effect the moment
// the user marks a conversation, before the on-chain signal is harvested back. OR-combined with the
// on-chain reduction by isConversationSensitive().
void setLocalSensitivePredicate(std::function<bool(const std::string&)> pred) {
local_sensitive_pred_ = std::move(pred);
}
// True if this conversation is sensitive (locally marked OR on-chain). Its rows are stored under the
// local vault key; while that key is absent its messages are withheld from the store on ingest/load.
bool isConversationSensitive(const std::string& conversationId) const;
// --- Outgoing (compose) ---
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
std::string identityPublicKeyHex() const;
@@ -131,6 +142,7 @@ private:
ChatStore store_;
ChatDatabase* db_ = nullptr; // optional; not owned
std::function<bool(const std::string&)> blocked_pred_; // true => cid is blocked (drop its messages)
std::function<bool(const std::string&)> local_sensitive_pred_; // true => cid locally marked sensitive
// Reduced on-chain control state, cid -> latest signal. Not persisted (re-derived from chain).
struct ControlState {