Skip to content

CogneeWrapper collapses cross-client private:* writes on singleton wrapper #367

@thinmintdev

Description

@thinmintdev

Surfaced by

#317 / PR #366. REST /api/memory/add was fixed to resolve private:<agent> from X-hal0-Agent + X-hal0-Private headers and pass that dataset to the wrapper. The route is correct; the wrapper layer still collapses.

Repro (post #366)

# REST writes resolve to private:hermes-agent.
curl -X POST http://127.0.0.1:8080/api/memory/add \
  -H 'X-hal0-Agent: hermes-agent' -H 'X-hal0-Private: 1' \
  -H 'Content-Type: application/json' \
  -d '{"text":"probe"}'

# But the actual Cognee dataset is still `shared` — `CogneeWrapper._effective_write_dataset`
# (src/hal0/memory/cognee_wrapper.py:869) collapses `private:*` to `shared` on a
# non-private wrapper. The singleton wrapper at app.state.memory_wrapper is created
# with client_id="anonymous", private_mode=False.

Root cause

CogneeWrapper was designed for one-instance-per-(client_id, private_mode) — see tests/memory/test_cognee_wrapper.py::test_private_writes_invisible_to_other_clients which constructs alice + bob wrappers. The wrapper guards against a non-private instance writing to private:<other> so a wrong-client wrapper can't smuggle data.

But production runs ONE wrapper (app.state.memory_wrapper, anonymous, non-private). The REST and MCP layers are the identity gate. The wrapper's collapse guard is now defending against itself.

Options (order: most-proper → blocker fix → spike)

a) Reshape CogneeWrapper so a single instance can serve multiple clients: take client_id + private per-call (audit log + scope filter both read from the per-call args, not the constructor). Touches every callsite + the audit log shape but is the truthful model.

b) Keep one wrapper but pass a trusted_caller=True kwarg to add / search / list_items from REST + MCP, signaling that the caller has already enforced identity. Wrapper-level collapse is skipped for trusted callers. Smaller diff, preserves the per-client wrapper code path for tests.

c) Stand up a per-request wrapper from a pool keyed by client_id. Simplest mental model but multiplies the Cognee-init cost.

Acceptance

  • Smoke curl POST /api/memory/add with private headers + curl POST /api/memory/search with same headers returns the just-written item.
  • hal0 agent bootstrap hermes memory_roundtrip smoke passes.
  • Existing tests/memory/ private-isolation contract (Alice can't see Bob's private) holds.

Tracked because

Closing the bootstrap smoke loop from #317 needs both halves. PR #366 ships the REST half so the route is no longer the bug, but the LXC smoke acceptance from #317 cannot succeed until this wrapper-side change lands too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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