Skip to content

Commit 2eb0c12

Browse files
Mount memory routes under /api/tenants/:tenantId (CL-4506) (#27)
Register add/search/list at /api/tenants/:tenantId/memory/* so a real hub createResolveTenant supplies principal + tenant — same tree as workflows and assets. Keep requirePrincipal as a fail-closed 401 when context is missing. Rewrite product docs around hub mount + agent/ingestion callers; drop MIGRATION.md and cutover noise. Tests use tenant-prefixed paths.
1 parent 619e91c commit 2eb0c12

15 files changed

Lines changed: 268 additions & 541 deletions

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Agent guide — @corbits/memory
22

3-
A library, not a service. `src/` is the whole product: a memory add / find /
4-
ask / recent SDK that **mounts onto a host Interchange app**. There is no server,
3+
A library, not a service. `src/` is the whole product: a memory **add / search /
4+
list** SDK that **mounts onto a host Interchange app**. There is no server,
55
port, or process entrypoint here, and there never should be.
66

7+
78
## Commands
89

910
```bash

‎ARCHITECTURE.md‎

Lines changed: 63 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,84 @@
11
# Corbits Memory — Architecture
22

3-
A memory add / find / ask / recent SDK that mounts onto an Interchange hub. The
4-
host owns auth, tenancy, and the process; this library owns the memory /
5-
vector plane and the routes that read and write it.
3+
A memory **add / search / list** SDK that mounts onto an Interchange hub. The
4+
host owns auth, tenancy, and the process; this library owns the durable memory
5+
plane and the protected routes that read and write it.
66

77
## Why an SDK, not a service
88

9-
The memory store was originally built inside a larger backend. It turned out
10-
to be cleanly detachable, and then cleanly *mountable*:
9+
The store was detachable from a larger backend, then mountable:
1110

12-
- No memory table has a foreign key into any control-plane table — every
13-
cross-reference (`tenant_id`, `principal_id`, source refs) is plain `text`.
14-
- Embedding and reranking go out as plain HTTP to configured model endpoints,
15-
not through any agent runtime.
16-
- The ACL rule is a self-contained scope stored on the row, not a join against
17-
a grant engine.
11+
- No memory table has a foreign key into any control-plane table — cross-refs
12+
(`tenant_id`, `principal_id`, source refs) are plain `text`.
13+
- Embedding and reranking go out as plain HTTP to configured model endpoints.
14+
- Document access is Interchange grant tags on the row (`accessTags` + creator),
15+
not a private ACL engine inside this package.
1816

19-
So the library needs nothing but a pgvector Postgres and an embed/rerank
20-
endpoint. It ships as `createMemory(opts)` — pass `app` to register HTTP. The host passes its
17+
It ships as `createMemory({ app, … })`: the host passes its Hono app and grant
18+
store; the library registers routes, reads identity from request context, and
19+
talks to its DocumentStore. No second server.
2120

22-
Hono app and its grant store; the library mounts its routes, reads identity from
23-
the request context, and talks to its own vector store. No second server, no
24-
HTTP hop.
21+
## Product path
22+
23+
```
24+
tools / ingestion → /api/tenants/:tenantId/memory/* → Memory plane → DocumentStore
25+
↑
26+
Interchange auth + principal + grants
27+
```
28+
29+
Mount is intentionally small. The host already has `app`, grants, and
30+
principal middleware; memory only needs to be handed those and the vector
31+
config (or an injected store).
2532

2633
## Boundaries
2734

28-
- **Runtime**: Bun + Hono, mounted on the host's app. **DB**: its own pgvector
29-
Postgres (`KNOWLEDGE_DATABASE_URL`). **Types**: arktype at every route
30-
boundary.
31-
- **No auth of its own.** The SDK authenticates nothing. Interchange resolves
32-
the caller (session, `cke_` API key, or MCP OAuth) and puts `principal` +
33-
`tenant` on the request context; each mounted route reads identity from there
34-
(`caller(c)` → `scopeId = principal.tenantId`, `subjectId = principal.id`).
35-
- **Grants delegate to the host.** Pass `grants` (`{ grantStore,
36-
conditionRegistry }`) and the SDK guards routes with Interchange's
37-
`createRequireGrant`.
38-
- **Dependencies** are public npm only — `@intx/hub-api` (`TenantEnv`,
39-
`createRequireGrant`), `@intx/authz` (`authorize`), `@intx/log`, Hono,
40-
Drizzle, arktype, `postgres`, `hono-openapi`. Eight total. LGPL-2.1-licensed —
41-
see `LICENSE`.
42-
43-
## Identity — read from context, stored as data
44-
45-
1. **Who is calling** is the request principal, read off the Interchange
46-
context. Clients never send `tenant_id`/`principal_id` — the handlers read
47-
only content fields (title/text/query/limit/access_tags/share) and take identity from context.
48-
2. **What is stored** is opaque data on every record: `tenant_id`,
49-
`principal_id`, `created_by_kind` (human/agent/system), `source_class`, and
50-
relations (the edge graph). Every query is scoped by `tenant_id` first; then
51-
document access uses Interchange grant tags (`accessTags` + creator).
52-
53-
Cross-tenant isolation is enforced at query time by `tenant_id`; document-level
54-
access is grant tags via `@intx/authz` (creator always allowed). This is the
55-
trust model.
56-
57-
## Layers
58-
59-
- `raw_capture` — immutable, append-only original content. The replay substrate.
60-
- `derived` — chunks / embeddings / authority / edges, all derived from
61-
`raw_capture`.
62-
- `transform_config` + replay — a named, versioned transform (chunk strategy,
63-
embed model, rerank endpoint, authority weights, MMR λ) that rebuilds the
64-
derived layer from raw without re-fetching source.
35+
- **Runtime**: Bun + Hono, mounted on the host app. **DB**: own pgvector
36+
Postgres (`KNOWLEDGE_DATABASE_URL`) unless `documentStore` is injected.
37+
**Types**: arktype at every route boundary.
38+
- **No auth of its own.** Interchange resolves the caller and puts `principal`
39+
+ `tenant` on context; routes read identity from there
40+
(`tenantId = principal.tenantId`, `principalId = principal.id`).
41+
- **Grants delegate to the host.** Pass `grantStore` + `conditionRegistry`;
42+
routes use `createRequireGrant("memory", action)`.
43+
- **Dependencies**: `@intx/hub-api`, `@intx/authz`, `@intx/log`, Hono, Drizzle,
44+
arktype, `postgres`, `hono-openapi`. LGPL-2.1 — see `LICENSE`.
6545

66-
## Mounted surface
46+
## Identity — context in, data out
47+
48+
1. **Who is calling** is the request principal. Clients never send
49+
`tenant_id` / `principal_id` on the body.
50+
2. **What is stored** is opaque data: `tenant_id`, `principal_id`,
51+
`created_by_kind`, `access_tags`, source refs. Queries scope by `tenant_id`
52+
first; document access is grant tags + creator.
6753

68-
`createMemory({ app })` adds, under the host app:
54+
## Layers (default pgvector store)
6955

70-
- `POST /api/memory/add` — ingest a note (raw + derive).
71-
- `POST /api/memory/search` — hybrid retrieval: FTS + dense (pgvector) → RRF
72-
fusion → cross-encoder rerank → bounded authority/recency boosts → MMR;
73-
optional live `SourceProvider` merge (fail-soft).
74-
- `GET /api/memory/list` — recent documents for the caller's scope,
75-
filtered with the same grant-tag access as local search (`canAccessDocument`).
56+
- `raw_capture` — immutable original content (replay substrate).
57+
- `derived` — chunks / embeddings / authority / edges from raw.
58+
- `transform_config` + replay — rebuild derived from raw without re-fetch.
59+
60+
Injected DocumentStores own their own persistence model; the plane still
61+
exposes the same three verbs.
62+
63+
## Mounted surface
7664

77-
It also returns an in-process `Memory` (`add`, `search`, `list`, `close`).
78-
There is no product `ask` / `remember` / `recall` and no host-injected
79-
`generate` on the plane — inference is host-owned and ephemeral (call your
80-
model, then `add` / `search`).
65+
`createMemory({ app })` registers:
8166

82-
MCP is not part of this package — mount `@corbitsdev/hono-openapi-mcp` to expose
83-
these routes as MCP tools.
67+
- `POST /api/tenants/:tenantId/memory/add` — ingest (raw + derive on the default store).
68+
- `POST /api/tenants/:tenantId/memory/search` — hybrid retrieval (FTS + dense → RRF → rerank →
69+
authority/recency → MMR); optional live `SourceProvider` merge (fail-soft).
70+
- `GET /api/tenants/:tenantId/memory/list` — recent documents, same grant-tag filter as local
71+
search.
8472

85-
External ingestion (Linear, GitHub, …) is not a route here — the host
86-
authenticates the forwarder to Interchange and calls `plane.add` / a
87-
`SourceProvider` mapper, or mounts HTTP add after its own auth.
73+
Returns an in-process `Memory` (`add`, `search`, `list`, `close`) for host
74+
workers and ingestion modules that already resolved identity.
8875

89-
Legacy paths `/capture`, `/search` (old knowledge), `/timeline`, `/find`,
90-
`/ask`, `/recent` are not mounted (hard cutover).
76+
**Agent tools are not in this package.** Routes are OpenAPI-described
77+
(`describeRoute`). The host mounts `@corbitsdev/hono-openapi-mcp` (or any
78+
OpenAPI→tools bridge) so agents call these routes under Interchange auth.
9179

9280
## Provenance
9381

94-
The framework-agnostic core (chunk strategies, embed client + model registry,
95-
authority weighting, hybrid search, MMR, rerank client, ingestion adapters) was
96-
extracted from an internal RAG implementation and generalized. The persistence
97-
and the mountable surface are native to this repo.
82+
Framework-agnostic core (chunking, embed/rerank clients, hybrid search, MMR)
83+
was extracted from an internal RAG implementation. Persistence and the
84+
mountable surface are native to this repo.

‎CHANGELOG.md‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
`registerMemoryRoutes`.
1515

1616
`createMemory`, `loadMemoryConfig`, `runMemoryMigrations`, `Memory`,
17-
`MemoryConfig`, `MemoryError`. HTTP paths are under `/api/memory/`; grants are
17+
`MemoryConfig`, `MemoryError`. HTTP paths are under
18+
`/api/tenants/:tenantId/memory/`; grants are
1819
`memory:add` / `memory:search`; access tags use `memory.owner:` / `memory.tenant:`
19-
/ `memory.space:`. Postgres schema name remains `knowledge`. See `MIGRATION.md`.
20+
/ `memory.space:`. Postgres schema name remains `knowledge`.
2021
- **Breaking:** memory plane surface is `add` / `search` / `list` with
21-
`principalId` + `tenantId` only. Removed product verbs: `find` (→`search`),
22-
`recent` (→`list`), `ask`, `remember`, `recall`, and any `MemoryProvider` /
23-
`generate` path. Inference is host-owned. See `MIGRATION.md`.
24-
- **Breaking:** HTTP routes are `POST /api/memory/add`,
25-
`POST /api/memory/search`, `GET /api/memory/list`. Old paths are not mounted.
22+
`principalId` + `tenantId` only. Inference is host-owned (no answer endpoint
23+
or personal-memory side-channel on the plane).
24+
25+
- **Breaking:** HTTP routes are `POST /api/tenants/:tenantId/memory/add`,
26+
`POST /api/tenants/:tenantId/memory/search`,
27+
`GET /api/tenants/:tenantId/memory/list` (inherits hub `resolveTenant`).
28+
Old unscoped `/api/memory/*` paths are not mounted.
2629
- **Breaking:** grant actions are `add` and `search` (was `capture` / `find` /
2730
knowledge `search`). `list` uses the `search` grant. Capability resource is
2831
`memory`. Document-tag checks use action `search`.
@@ -42,13 +45,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4245
- Optional `TextExtractor` + `file` XOR `content` on `add`
4346
- `share` sugar on `add` (maps to access tags only: owner, tenant, peers)
4447
- `access_tags` on `knowledge.document` (baseline schema; Postgres schema name unchanged)
45-
- `MIGRATION.md` hard-cutover notes for in-repo consumers
4648

4749
### Removed
4850

49-
- Product `ask` / `remember` / `recall` and `MemoryProvider` side-channel
50-
- Host-injected `generate` on the plane (use host inference + `add` / `search`)
51-
- HTTP `POST /api/memory/ask`, `POST /api/memory/find`, `GET /api/memory/recent`
51+
- Host-injected generate path on the plane (use host inference + `add` / `search`)
5252

5353
## [0.1.2] — 2026-07-31
5454

‎IMPLEMENTATION.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ returns the run summary either way.
329329
this repo; chunks left unembedded simply never populate the dense
330330
channel for the query — they're still found by lexical/FTS). Any of these
331331
failure modes sets `degraded: true` on the `CaptureResult`, surfaced by
332-
`POST /api/memory/add` as a `degraded` field in its response — the add
332+
`POST /api/tenants/:tenantId/memory/add` as a `degraded` field in its response — the add
333333
still succeeded (chunks are durable and lexically searchable), only the
334334
dense/vector channel for those chunks is incomplete.
335335

@@ -486,12 +486,15 @@ Each route is guarded with `grantGuard(deps, action)`, which applies the host's
486486

487487
| Method + path | Grant action | Request body | Response |
488488
|---|---|---|---|
489-
| `POST /api/memory/add` | `add` | `{ title, text, access_tags?, share? }` | `200 { documentId }`; `400` on validation |
490-
| `POST /api/memory/search` | `search` | `{ query, limit?, kinds?, entity_ids? }` (limit 1–50; `kinds`/`entity_ids` narrow every retrieval channel — lexical and dense — to a document `kind` or linked entity id before fusion; unset or `[]` = unfiltered) | `200 { items[], evidence?, degraded? }`; `400` on bad input |
491-
| `GET /api/memory/list` | `search` | — | `200 { events: [{ at, title, source, tenantId, principalId }] }` — durable recent documents for the caller's scope, filtered with grant-tag access (`canAccessDocument`). One event per document (active live version). |
489+
| `POST /api/tenants/:tenantId/memory/add` | `add` | `{ title, text, access_tags?, share? }` | `200 { documentId }`; `400` on validation |
490+
| `POST /api/tenants/:tenantId/memory/search` | `search` | `{ query, limit?, kinds?, entity_ids? }` (limit 1–50; `kinds`/`entity_ids` narrow every retrieval channel — lexical and dense — to a document `kind` or linked entity id before fusion; unset or `[]` = unfiltered) | `200 { items[], evidence?, degraded? }`; `400` on bad input |
491+
| `GET /api/tenants/:tenantId/memory/list` | `search` | — | `200 { events: [{ at, title, source, tenantId, principalId }] }` — durable recent documents for the caller's scope, filtered with grant-tag access (`canAccessDocument`). One event per document (active live version). |
492+
493+
`registerMemoryRoutes` and `createMemory({ app })` register the three HTTP routes.
494+
Agent tools are a host concern — mount `@corbitsdev/hono-openapi-mcp` (or any
495+
OpenAPI→tools bridge) against the same app. The plane surface is only
496+
`add` / `search` / `list` (plus `close`); inference stays on the host.
492497

493-
`registerMemoryRoutes` and `createMemory({ app })` register the three HTTP routes. MCP is a separate package (`@corbitsdev/hono-openapi-mcp`).
494-
There is no product `ask` / `remember` / `recall` HTTP or plane surface.
495498

496499

497500
### Timeline wire fields (vs the old CaptureLog ring)

‎MIGRATION.md‎

Lines changed: 0 additions & 104 deletions
This file was deleted.

0 commit comments

Comments
 (0)