From 5593845c5d8818147058e288e7c5ad19626db9d6 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 21 Sep 2026 21:06:41 -0700 Subject: [PATCH 1/2] docs(readme): Quickstart shows real host usage (CL-8775) --- README.md | 149 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 109 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index c7632b2..963ff5a 100644 --- a/README.md +++ b/README.md @@ -24,59 +24,98 @@ yarn add @corbits/memory bun add @corbits/memory ``` -```ts -import { createMemory, loadMemoryConfig } from "@corbits/memory"; +Write the mount as a function that takes your hub's `app`, `grantStore`, and +`conditionRegistry` — the same trio you already pass to +`createRequireGrant`/`createApp`: -const memory = createMemory({ - app, // your Hono app — routes register under /api/tenants/:tenantId/memory/* - config: loadMemoryConfig(), // DATABASE_URL + embed env - grantStore, // your Interchange grant store (required for the HTTP mount) - conditionRegistry, // your condition registry -}); +```ts +import { Hono } from "hono"; +import type { TenantEnv } from "@intx/hub-api"; +import type { ConditionRegistry, GrantStore } from "@intx/authz"; +import { createMemory, loadMemoryConfig, type Memory } from "@corbits/memory"; + +export function installMemory( + app: Hono, + grantStore: GrantStore, + conditionRegistry: ConditionRegistry, +): Memory { + const memoryApp = new Hono(); + const memory = createMemory({ + app: memoryApp, + config: loadMemoryConfig(), // DATABASE_URL + embed env — see below + grantStore, + conditionRegistry, + }); + app.route("/", memoryApp); + return memory; +} ``` -That registers the tenant routes. Identity is `c.get("principal")` — bodies -never carry tenant or principal. Missing principal → 401. Missing grant → -403. +Mount `installMemory` below the middleware that sets `principal`/`tenant` +(a real hub's `createResolveTenant` on `/api/tenants/:tenantId/*` already +does). Identity comes from `c.get("principal")` — request bodies never +carry tenant or principal. Missing principal → 401, missing grant → 403. -In-process, no HTTP and no Postgres — uses the exported fake store. Creator -always sees their own documents. +Apply migrations before serving traffic: ```ts -import { createMemory, createFakeDocumentStore } from "@corbits/memory"; - -const memory = createMemory({ - documentStore: createFakeDocumentStore(), -}); +import { runMemoryMigrations } from "@corbits/memory/migrations"; -await memory.add({ - tenantId: "acme", - principalId: "alice", - content: { - title: "Deploy notes", - text: "Staging deploys run from main.", - }, -}); +await runMemoryMigrations(process.env.DATABASE_URL!); +``` -const { items } = await memory.search({ - tenantId: "acme", - principalId: "alice", - query: "staging", -}); +`loadMemoryConfig()` reads `DATABASE_URL` (required — tables live in a +`memory` Postgres schema, pgvector-capable) plus the embed pair: +`EMBED_BASE_URL`/`EMBED_MODEL` (OpenAI-compatible by default; `EMBED_API_KEY` +optional, `EMBED_API_STYLE`/`EMBED_TIMEOUT_MS` to override). Set both to +enable dense retrieval, or leave both unset to run lexical-only — full-text +search with no embed endpoint, a fully-supported mode rather than a degraded +one. Setting exactly one throws at load time. There is no credential-based +fallback: the embed pair is deployment config, resolved once per process +from the environment, the same for every tenant it serves. `RERANK_BASE_URL`/ +`RERANK_MODEL`/`RERANK_API_KEY` are the equivalent optional pair for +reranking. See `.env.example` in this repo for the full list. -console.log(items.map((item) => item.title)); -``` +### Deployed agents (run-scoped routes) -On a real hub, omit `documentStore` and pass `config: loadMemoryConfig()` -(needs `DATABASE_URL`, `EMBED_BASE_URL`, `EMBED_MODEL`; see `.env.example`). -Apply migrations first: +A deployed agent authenticates with its own sidecar bearer token, not a +browser session, so it gets a second mount, scoped to the run rather than to +a tenant-session request: ```ts -import { runMemoryMigrations } from "@corbits/memory/migrations"; - -await runMemoryMigrations(process.env.DATABASE_URL!); +import { Hono } from "hono"; +import { + mountWorkflowMemory, + type Memory, + type WorkflowMemoryEnv, +} from "@corbits/memory"; + +export function installWorkflowMemory( + app: Hono, + memory: Memory, + agentToken: { + verify: ( + ctx: unknown, + ) => Promise<{ tenantId: string; definitionId: string } | undefined>; + resolveRun: ( + runAddress: string, + ) => Promise<{ + tenantId: string; + principalId: string; + runId: string; + } | null>; + }, +): void { + const workflowMemoryApi = new Hono(); + mountWorkflowMemory(workflowMemoryApi, { memory, agentToken }); + app.route("/api/workflow-memory", workflowMemoryApi); +} ``` +`memory` here is the same plane `installMemory` built — one engine, two +mounts. The tool definitions a deployed agent calls against this mount ship +at `@corbits/memory/sidecar-bundle`. + ## How it works `createMemory` builds the plane. Pass `app` to register @@ -92,6 +131,34 @@ docs. Details: [`docs/AUTHZ-DOCUMENT-ACCESS.md`](docs/AUTHZ-DOCUMENT-ACCESS.md). The resident distiller (`createResidentDistiller` / `runDistillTick`) is at `@corbits/memory/distiller`. +### Lower-level: in-process calls, no HTTP + +`app` is optional. Passing only `config` builds the plane without +registering routes, for a host worker that calls `add`/`search` directly +(the resident distiller does this): + +```ts +import { createMemory, loadMemoryConfig } from "@corbits/memory"; + +const memory = createMemory({ config: loadMemoryConfig() }); + +await memory.add({ + tenantId: "acme", + principalId: "alice", + content: { title: "Deploy notes", text: "Staging deploys run from main." }, +}); + +const { items } = await memory.search({ + tenantId: "acme", + principalId: "alice", + query: "staging", +}); +``` + +Without `grantStore`, search/list fall back to creator-only visibility — a +safe default for a standalone caller, but not the shared-document behavior a +real tenant gets through `installMemory` above. + ## Development ```bash @@ -102,7 +169,9 @@ bun run typecheck # tsc --noEmit bun run test # bun test ./src ``` -There is no `build` script — the published surface is `src/`. +Tests use `createFakeDocumentStore`/`createFakeSourceProvider` (exported for +this purpose) so the suite runs without Postgres. There is no `build` +script — the published surface is `src/`. ## License From b6d37337eb734bb0dedd37a3327740dd836acb04 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 21 Sep 2026 21:14:33 -0700 Subject: [PATCH 2/2] docs(readme): no non-null assertion on DATABASE_URL, fail fast instead (CL-8775) --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 963ff5a..6009b42 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,15 @@ Apply migrations before serving traffic: ```ts import { runMemoryMigrations } from "@corbits/memory/migrations"; -await runMemoryMigrations(process.env.DATABASE_URL!); +export async function migrateMemory(databaseUrl: string): Promise { + await runMemoryMigrations(databaseUrl); +} + +const databaseUrl = process.env.DATABASE_URL; +if (databaseUrl === undefined) { + throw new Error("DATABASE_URL is required to run memory migrations"); +} +await migrateMemory(databaseUrl); ``` `loadMemoryConfig()` reads `DATABASE_URL` (required — tables live in a @@ -97,9 +105,7 @@ export function installWorkflowMemory( verify: ( ctx: unknown, ) => Promise<{ tenantId: string; definitionId: string } | undefined>; - resolveRun: ( - runAddress: string, - ) => Promise<{ + resolveRun: (runAddress: string) => Promise<{ tenantId: string; principalId: string; runId: string;