Metadata-Version: 2.5
Name: curu-memory
Version: 0.2.0
Summary: A shared knowledge base and GraphRAG store for ComfyUI and generative media, served to agents over MCP.
Project-URL: Repository, https://github.com/darth-veitcher/curu-memory
Project-URL: Documentation, https://github.com/darth-veitcher/curu-memory#readme
Project-URL: Changelog, https://github.com/darth-veitcher/curu-memory/blob/main/CHANGELOG.md
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: httpx2>=2.10
Requires-Dist: markdown-it-py>=4.0
Requires-Dist: mcp==2.3.0
Requires-Dist: pydantic>=2.12
Requires-Dist: pyyaml>=6.0
Requires-Dist: starlette>=1.0
Requires-Dist: uvicorn>=0.31.1
Description-Content-Type: text/markdown

# curu-memory

## What is this

curu-memory is a shared knowledge base for ComfyUI and generative media. It reads OKF concept files about workflows, models, parameters, prompt techniques and findings. Then it indexes them and serves them to agents through MCP and to persons through a command. An agent can search the knowledge, read a concept and follow its links to related concepts. The knowledge stays in plain Markdown files that you keep in git.

## What you can do

| Task | Command | Section |
|---|---|---|
| Write a concept for the index | Add `<name>.md` with front matter | [Write a concept file](#write-a-concept-file) |
| Add images, audio and video to a concept | Add a `media:` list to the front matter | [Add media to a concept](#add-media-to-a-concept) |
| Build the index from your concept files | `curu-memory index --concepts ./knowledge` | [Index your knowledge](#index-your-knowledge) |
| Keep team notes and personal notes apart from the global knowledge | `curu-memory index --space personal/me` | [Keep team and personal knowledge apart](#keep-team-and-personal-knowledge-apart) |
| Give each person their own spaces, and see who reads which space | `curu-memory access person add me` | [Give each person their own spaces](#give-each-person-their-own-spaces) |
| Connect agents with tokens of Authentik or of the shipped rauthy | Set `CURU_MEMORY_OIDC_ISSUER` | [Connect agents with OIDC tokens](#connect-agents-with-oidc-tokens) |
| Give an agent a role in one space for some days | `curu-memory access grant scout --space team/look --role read` | [Give an agent its own access](#give-an-agent-its-own-access) |
| Search the knowledge from a terminal | `curu-memory search "VAE for WAN2.2 5B"` | [Search the knowledge](#search-the-knowledge) |
| Give the knowledge to a local agent | `curu-memory serve` | [Connect a local agent](#connect-a-local-agent) |
| Give the knowledge to a remote agent | `curu-memory serve --transport http` | [Connect a remote agent](#connect-a-remote-agent) |
| Search with an image, a clip or a video, for example an output of a workflow | `curu-memory search --media output.png` | [Search with media](#search-with-media) |
| Search by meaning with EmbeddingGemma 2 | Set `CURU_MEMORY_EMBEDDER_URL` | [Search by meaning](#search-by-meaning) |
| Browse the knowledge in a web browser | `curu-memory browse` | [Browse the knowledge](#browse-the-knowledge) |
| Read a concept and its links | The `get_concept` and `concept_neighbours` tools | [Read a concept and follow its links](#read-a-concept-and-follow-its-links) |
| Find why a call failed | The `code` of the error | [Handle errors](#handle-errors) |

## Install

You need Python 3.12 or later and [uv](https://docs.astral.sh/uv/).

```bash
uv tool install git+https://github.com/darth-veitcher/curu-memory
```

This installs the `curu-memory` command.

To install a release from PyPI, give a version range:

```bash
uv tool install "curu-memory>=0.2,<0.3"
```

A program that uses curu-memory as a library pins the same range in its `pyproject.toml`. A new minor version can change the Python API or the MCP tools, so the range stops before the next minor version.

## Quickstart

This example makes one concept, indexes it, searches it, and gives it to Claude Code. Make a directory for your concept files:

```bash
mkdir -p knowledge
```

Save this text as `knowledge/wan22-5b-vae.md`:

```markdown
---
type: Model Component
title: WAN2.2 5B VAE
description: "The VAE that the WAN2.2 5B model loads."
tags: [vae, wan]
status: verified
sources:
  - id: the-card
    resource: https://huggingface.co/Wan-AI
    author: person:owner
    last_modified: "2026-10-01"
---
The [WAN2.2 5B family](./wan22-5b.md "USES_COMPONENT") loads this VAE.

## Settings

Use the `wan2.2_vae.safetensors` file.
```

Then index the directory and search it:

```bash
curu-memory index --concepts ./knowledge
curu-memory search "VAE for WAN2.2 5B" --limit 5
```

The first command prints a report. `skipped: 0` means that each concept file is in the index. `broken links: 1` means that `wan22-5b.md` is not in the directory yet. The second command prints the concept, with its sources.

Then give the knowledge to Claude Code:

```bash
claude mcp add curu-memory -- curu-memory serve
```

The server reads the index in the same data directory as the index command. Claude Code can now use the `search_knowledge`, `search_knowledge_by_media`, `get_concept` and `concept_neighbours` tools.

## Write a concept file

A concept file is a Markdown file with YAML front matter between two `---` lines. The file in the [Quickstart](#quickstart) is an example. The front matter must have `description`. `type`, `title`, `tags`, `sources`, `status` and `media` are optional.

- The identifier of a concept is its file name without `.md`. In the example, it is `wan22-5b-vae`.
- A missing `title` becomes the identifier. A missing `type` becomes `Untyped`, and the index report gives a warning.
- A link to another concept is a Markdown link to its `.md` file. Its title in quotes is the link kind.
- The link kinds are `USES_COMPONENT`, `HAS_TURBO_VARIANT`, `SAME_LINEAGE_AS`, `SHARES_CLIP_TYPE_LABEL_WITH`, `EVIDENCED_BY`, `SHARES_GUARD_CONVENTION_WITH`, `DISTINCT_FAILURE_CLASS_FROM` and `RETRACTS_CLAIM_IN`.
- A link resolves by file name in all the subdirectories, so two concept files must not have the same name.
- Write each `##` heading for one topic. Search by meaning compares each section separately.

### Add media to a concept

Add example outputs, reference images, audio clips or videos to a concept with a `media` list. Each path is relative to the concept file:

```markdown
---
type: Finding
description: A CFG above 7 burns the colours of WAN2.2 5B.
media:
  - images/cfg-9-burnt.png
  - images/cfg-5-good.png
  - audio/cfg-notes.flac
  - video/cfg-9-motion.mp4
---
```

- The index command embeds each image, clip and video in the same space as the text, so a question in words can find it. This needs an embedder that accepts these media, for example EmbeddingGemma 2 on SGLang.
- An image must be a PNG, JPEG or WebP file. A clip must be a WAV, FLAC, MP3 or Ogg file. A video must be an MP4, MOV, WebM or MKV file. Each file must be 10 MB or less. The command reads the format from the first bytes of the file, not from its name.
- Keep one topic in each clip. A long clip with many topics matches each topic less well.
- The model sees 1 frame of a video each second, at most 32 frames. A longer video gets 32 frames, spread evenly, so a short event in it can be lost.
- Each path must stay in the concept directory, also through a symbolic link. Keep the media files in git with the concept files.
- A media file with a problem gives a warning. The concept stays in the index with its text.
- curu ignores the `media` key, so the same concept files still index in curu.

## Index your knowledge

Run the index command after each change to the concept files:

```bash
curu-memory index --concepts ./knowledge
```

You can also set `CURU_MEMORY_CONCEPTS_DIR` and run `curu-memory index` with no option. Add `--json` to get the report as a JSON object.

The report gives these values:

| Value | Meaning |
|---|---|
| `files read` | The `.md` files in the directory and its subdirectories. A `README.md` file and the files in a hidden directory, for example `.git`, are not concepts. |
| `concepts indexed` | The concepts in the new index. It is `files read` minus `skipped`. |
| `skipped` | The files that are not in the index. The report gives the file, the line and the reason for each file. |
| `warnings` | Problems that did not stop a file, for example a missing `type`. |
| `broken links` | Links to a concept that is in no space. A link to a concept of another space is not broken. The concept stays in the index. |
| `embedder` | The embedding model of the index, or `none` for an index of words only. |
| `media embedded` | The images, clips and videos that a search by meaning can find. |
| `media skipped` | The images, clips and videos that a search by meaning cannot find. Each one is also a warning, with the path and the reason. |
| `index` | The path of the index file in the data directory. |

The command exits with 0 when it skipped no file, and with 1 when it skipped one or more files. The command skips a file for one of these causes:

- The file is not UTF-8 text, or it has no front matter.
- The front matter has no closing `---` line, is not valid YAML, or is not a YAML mapping.
- The front matter has no `description`.
- The body has a `[[wiki-link]]`, or a link with no kind or an unknown kind.
- Two files have the same name. The command skips both files.
- The identifier of the concept is in another space. The reason names that space.

A media file with a problem gives a warning. The command does not skip the concept. It does not put the file in the index for one of these causes:

- The file does not exist, or the path is not a file.
- The path goes outside the concept directory, or it is an absolute path.
- The file is larger than 10 MB, or it is not an image, a clip or a video of a known format.
- The `media` value is not a list of paths. The command ignores the full list, and `media skipped` does not count it.

When no embedder is set, or the embedder did not embed a file, the file has no embedding. It stays in the index, and the browser shows it or plays it. A search by meaning cannot find it, and the report gives a warning. An embedder that accepts only text gives this warning for each file.

The command sends each image, clip and video to the embedder one time, with no task prompt. It keeps each vector in an embedding cache. The next run sends only the changed text and media. The same files always give the same index. The command writes a new index file and then replaces the old one. A server that runs at that time uses the old index until the new index is complete. If the embedder fails on a text, the old index stays.

## Keep team and personal knowledge apart

Give each part of the knowledge its own space. Index each directory into its space:

```bash
curu-memory index --concepts ./knowledge
curu-memory index --concepts ./team-notes --space team/look
curu-memory index --concepts ./my-notes --space personal/me
```

- A space is `global`, `team/<slug>` or `personal/<handle>`. The default is `global`.
- The [team slug](#glossary) is the short name of the team, and the [handle](#glossary) is the short name of the person. Each has 1 to 40 characters from `a` to `z`, `0` to `9` and `-`. The first character is a letter.
- Each space has its own index file. An index run changes only the index file of its space. All spaces share the embedding cache.
- A concept identifier must be unique in all spaces. The command skips a concept whose identifier is in another space, and the report names that space.
- All spaces must use one embedder model. When the embedder model is not the model of another space, the command stops with exit code 1 and changes no index. The command accepts a space with no embeddings. A search of that space uses words only.
- Only one index run can run at a time. A second run stops with exit code 1 and the message "Another index run holds the lock".
- A bad space name stops the command with exit code 2.

A caller reads only its readable spaces: the spaces where its principal has a role. A caller with no principal reads `global` only. See [Give each person their own spaces](#give-each-person-their-own-spaces). A search finds concepts in each readable space, and each result names its space. `concept_neighbours` and the browser resolve a link to a concept in another readable space.

> **Warning:** The index of a team space or a personal space is not encrypted. Each person who can read the data directory can read it.

## Give each person their own spaces

Register each person and each team. The first person is an [instance owner](#glossary):

```bash
curu-memory access person add me
curu-memory access person add guest
curu-memory access team add look
curu-memory access member add look guest
```

Then set `CURU_MEMORY_PRINCIPAL` to the handle of the person of each `serve`, `browse` or `search` process. For example, set `CURU_MEMORY_PRINCIPAL=me` in the environment of the MCP client. A registered [principal](#glossary) reads only the spaces where it has a [role](#glossary). With no principal, a process reads `global` only:

| Space | Role of each person |
|---|---|
| `global` | `admin` for an instance owner, else `read`. |
| `team/<slug>` | `admin` for an instance owner, `write` for a member of the team, else no role. |
| `personal/<handle>` | `admin` for that person only. An instance owner has no role in the personal space of another person. |

- Without `CURU_MEMORY_PRINCIPAL`, a process reads `global` only.
- A `CURU_MEMORY_PRINCIPAL` that names no registered person stops `serve`, `browse` and `search` with exit code 78.
- A value that is not a valid handle stops each command with exit code 78.
- A running server reads the access store at each request. When you remove a person, that person reads nothing from the next request. A tool then gives the error `access_refused`, and the browser shows the page "No access" with HTTP 403. A media request gets HTTP 404. Each such request writes a refused line to the audit log.
- `curu-memory access person add HANDLE --owner` registers another instance owner. The command refuses to remove the last instance owner.
- `curu-memory access person remove`, `access team remove` and `access member remove` remove a person, a team or a member. When you remove a person or a team, the command also removes its team memberships.
- A removed person or team keeps the index of its space. The command says so. Delete its index file in the data directory, for example `personal--me.sqlite` or `team--look.sqlite`. Else a new person or team with the same handle or slug reads it.
- A bad handle or slug gives exit code 2. A change that the rules refuse, for example a new member of a team that does not exist, gives exit code 1.

Show the persons, the teams and the role of each person in each space:

```bash
curu-memory access show
curu-memory access show --json
```

The access store is `access.sqlite` in the data directory. It holds handles and slugs, and no token and no secret. The roles `write` and `admin` give no more access than `read` now. A later version uses them when persons and agents can change knowledge.

### Read the audit log

`audit.jsonl` in the data directory gets one JSON line for each change of access and each refused read. A refused read is a request for a concept, its links, its page or its media in a space without a role. A request of a principal with no role in any space is also a refused read. The caller gets the same answer as for a concept that does not exist.

| Key | Value |
|---|---|
| `time` | The UTC time, for example `2026-10-31T21:00:00Z`. |
| `principal` | `person:<handle>`, `agent:<name>`, `anonymous` for a process with no principal or for a refused token, or `operator` for the command `access`. |
| `acts_for` | The person that an agent acts for, or `null`. |
| `action` | For example `person.add`, `person.link`, `member.remove`, `agent.add`, `grant.add`, `grant.revoke`, `get_concept`, `concept_neighbours`, `page`, `media` or `token`. |
| `space` | The space of the change, of the grant or of the refused read. `null` for a change of a person or an agent, for a principal with no role in any space, and for a refused token. |
| `outcome` | `changed` or `refused`. |
| `reason` | For a change, the message that the command prints, for example `added the person me`. For a refused read, `no role in the space` or `no role in any space`. For a refused token, the reason of the token check, for example `expired`, `wrong audience` or `unknown subject`. |
| `subject` | The handle, the slug or the agent name of a change, or `<slug>:<handle>` for a member change. `null` for a refused read or a refused token. |

A line never holds a token, a question, a concept identifier, a title or media. curu-memory makes the file with mode 0600. If curu-memory cannot write a line, it writes an error to stderr, and the request gets its normal answer. Rotate it with `logrotate` and its `copytruncate` option.

## Search the knowledge

```bash
curu-memory search "VAE for WAN2.2 5B" --limit 5
curu-memory search "turbo" --type "Model Component" --tag wan
```

- `--limit` sets the number of results, from 1 to 50. The default is 10.
- `--type` keeps only concepts of that OKF type. Repeat it for more types.
- `--tag` keeps only concepts that have the tag. Repeat it, and each concept must have all the tags.
- `--json` prints the same JSON object as the `search_knowledge` tool.

Each result gives the identifier, the space, the title, the type, the description, the tags and the sources. The `status` is the `status` of the front matter. The `score` comes from [reciprocal rank fusion](#glossary), from about 0.006 to 0.018. A higher score is a better match. The best match of words alone has a score of about 0.016. A concept that is first in words and in meaning has a score of about 0.018.

`matched_unit` is the unit that matched the meaning best. `summary` is the title and the description. `overview` is the text before the first heading. `image.1` is an image at the first place of the `media` list. `audio.2` is a clip at the second place, and `video.3` is a video at the third place. Other values are the slug of a `##` heading. A repeated slug gets a number, for example `notes-2`. A `## Summary` heading is `summary-2`. `matched_kind` is `text` for the summary, the overview or a section, `image` for an image, `audio` for a clip and `video` for a video. Both are null when only the words matched the concept, for example in `words_only` mode.

The `mode` line says how the search worked. `hybrid` means words and meaning. `words_only` means words only, and the `notice` line says why. With two or more spaces, a space whose vectors this embedder cannot use is searched by words only. An example is a space with no vectors. When another space can use its vectors, the `mode` stays `hybrid`, and the `notice` names the spaces of words. The `notice` line also shows when nothing matched. A name such as `sdxl_base_1.0` or `wan2.2_vae.safetensors` matches as one word.

The printed result gives the `matched` line of each concept that matched by meaning, for example `matched: image.1 (image)`.

## Search with media

Give an image, an audio clip or a video in place of a question, for example an output of a workflow that has a fault:

```bash
curu-memory search --media output.png --limit 5
curu-memory search --media voice.flac
curu-memory search --media motion.mp4
```

- The file must be a PNG, JPEG, WebP, WAV, FLAC, MP3, Ogg, MP4, MOV, WebM or MKV file of 10 MB or less. The command reads the kind from the first bytes.
- An image finds the concepts whose images or texts are nearest in meaning. A clip finds the concepts whose clips or texts are nearest, and a video finds videos and texts. Each kind of question finds only its own kind of media. `matched_kind` says which kind matched.
- Media has no words, so only the meaning counts. The `mode` is `media`. The `score` is from about 0.009 to 0.016. The first result has about 0.016.
- Give a question or `--media`, not both. Both, or neither, stop the command with exit code 2.
- A bad file stops the command with exit code 2. A bad file is a missing file, a file larger than 10 MB, or a file of an unknown format. The tool gives the error `invalid_argument`, and `details.field` is `media`. See [Handle errors](#handle-errors).
- The search needs an embedder that accepts the kind of the file. Without one, the result is empty, and the `notice` says why.
- `--limit`, `--type`, `--tag` and `--json` work as for a question in words.

An agent uses the tool `search_knowledge_by_media`. It gives the file as [base64](#glossary) text or as a base64 [data URI](#glossary), for example `data:image/png;base64,...`. The base64 text can have line breaks. It never gives a path. Over HTTP, a request can hold a file of 10 MB as base64.

## Search by meaning

Without an embedder, curu-memory searches by words only. To search by meaning too, run an [embedder](#glossary) that serves `POST /v1/embeddings`. SGLang, vLLM, Ollama and llama-server serve this API. The default model is [EmbeddingGemma 2](https://huggingface.co/google/embeddinggemma-2) from Google. It changes text, code, images, video and audio into embeddings that you can compare with each other. It is free to use under Apache 2.0.

For example, serve EmbeddingGemma 2 with SGLang on port 30000:

```bash
sglang serve --model-path google/embeddinggemma-2 --host 127.0.0.1
```

Then set the URL before you index, before you search and before you start the server:

```bash
export CURU_MEMORY_EMBEDDER_URL=http://127.0.0.1:30000
curu-memory index --concepts ./knowledge
```

Ollama gives the same model a different name, so set the name too:

```bash
ollama pull embeddinggemma-2
export CURU_MEMORY_EMBEDDER_URL=http://127.0.0.1:11434
export CURU_MEMORY_EMBEDDER_MODEL=embeddinggemma-2
```

- `CURU_MEMORY_EMBEDDER_URL` is the base URL of the embedder. A URL that ends with `/v1` also works.
- `CURU_MEMORY_EMBEDDER_MODEL` is the model name that the embedder serves. The default is `google/embeddinggemma-2`. Without a URL, this setting stops each command with exit code 78.
- `CURU_MEMORY_EMBEDDER_TOKEN` is an optional bearer token for the embedder.
- `CURU_MEMORY_EMBEDDER_TIMEOUT` is the time in seconds before a search uses words only. The default is 5. The index command uses the same limit for each request of 32 texts or 4 media files. Increase it for a slow embedder.
- `CURU_MEMORY_EMBEDDER_MIN_SIMILARITY` is the lowest [cosine similarity](#glossary) that counts as a match of meaning, from -1 to 1. The default is 0.68 for EmbeddingGemma and 0.35 for other models. A higher value gives fewer matches by meaning. A lower value gives more matches, and an unrelated question can get results. A value that is not a number from -1 to 1 stops each command with exit code 78.
- `CURU_MEMORY_EMBEDDER_MIN_IMAGE_SIMILARITY` is the same limit for a match between a question in words and an image. The default is 0.68 for EmbeddingGemma and 0.35 for other models. A higher value gives fewer matches with images. A lower value gives more matches, and an unrelated question can match an image. A value that is not a number from -1 to 1 stops each command with exit code 78.
- `CURU_MEMORY_EMBEDDER_MIN_AUDIO_SIMILARITY` is the same limit for a match between a question in words and a clip. The default is 0.68 for EmbeddingGemma and 0.35 for other models. A higher value gives fewer matches with clips. A lower value gives more matches, and an unrelated question can match a clip. A value that is not a number from -1 to 1 stops each command with exit code 78.
- `CURU_MEMORY_EMBEDDER_MIN_VIDEO_SIMILARITY` is the same limit for a match between a question in words and a video. The default is 0.71 for EmbeddingGemma and 0.35 for other models. A higher value gives fewer matches with videos. A lower value gives more matches, and a question can match a video with a near motion. Descriptions of the test videos scored from about 0.73 with their own videos, and wrong videos reached about 0.70. A value that is not a number from -1 to 1 stops each command with exit code 78.
- `CURU_MEMORY_EMBEDDER_MIN_IMAGE_TO_IMAGE_SIMILARITY` is the limit when you search with an image and the unit is an image. The default is 0.89 for EmbeddingGemma and 0.35 for other models. Images of the same content score above about 0.93. Different images can score up to about 0.85.
- `CURU_MEMORY_EMBEDDER_MIN_IMAGE_TO_TEXT_SIMILARITY` is the limit when you search with an image and the unit is a text. The default is 0.70 for EmbeddingGemma and 0.35 for other models. Correct descriptions of the test images scored from about 0.75. Wrong descriptions and unrelated concepts reached about 0.69.
- `CURU_MEMORY_EMBEDDER_MIN_AUDIO_TO_AUDIO_SIMILARITY` is the limit when you search with a clip and the unit is a clip. The default is 0.87 for EmbeddingGemma and 0.35 for other models. Clips of the same speech scored above about 0.94. Different speech scored up to about 0.80.
- `CURU_MEMORY_EMBEDDER_MIN_AUDIO_TO_TEXT_SIMILARITY` is the limit when you search with a clip and the unit is a text. The default is 0.70 for EmbeddingGemma and 0.35 for other models. Correct descriptions of the test clips scored from about 0.73. Wrong descriptions reached about 0.69, and unrelated concepts about 0.68.
- `CURU_MEMORY_EMBEDDER_MIN_VIDEO_TO_VIDEO_SIMILARITY` is the limit when you search with a video and the unit is a video. The default is 0.92 for EmbeddingGemma and 0.35 for other models. Videos of the same content scored above about 0.96. Different videos scored up to about 0.89.
- `CURU_MEMORY_EMBEDDER_MIN_VIDEO_TO_TEXT_SIMILARITY` is the limit when you search with a video and the unit is a text. The default is 0.73 for EmbeddingGemma and 0.35 for other models. Correct descriptions of the test videos scored from 0.737. Wrong descriptions reached 0.728 in an earlier measurement, and unrelated concepts 0.719.
- Each of these six settings must be a number from -1 to 1. A higher value gives fewer matches, and a lower value gives more. A value that is not a number from -1 to 1 stops each command with exit code 78.

### Set the task prompts

EmbeddingGemma needs a [task prompt](#glossary) around each text. curu-memory adds the prompts that Google gives for the model when the model name has `embeddinggemma` in it:

| Text | Prompt |
|---|---|
| A question | `task: search result \| query: {text}` |
| A section of a concept | `title: {title} \| text: {text}` |

`{text}` is the text. `{title}` is the title of the concept, and only the prompt of a section can use it. A different model gets the text with no prompt. Set `CURU_MEMORY_EMBEDDER_QUERY_PROMPT` for the prompt of a question, and `CURU_MEMORY_EMBEDDER_DOCUMENT_PROMPT` for the prompt of a section. Each prompt must contain `{text}`, or the command stops with exit code 78.

A search uses words only and says why in these cases:

- No embedder is set.
- The index has no embeddings, or it comes from a different model or other prompts.
- The embedder fails or does not answer in time.

An index from version 0.1.0 has no record of its prompts, so it counts as an index with no prompts. To use a different model or other prompts, run the index command again. The index command keeps an embedding cache in the data directory, so a second run sends only the changed text and media to the embedder.

## Connect a local agent

A local agent starts curu-memory itself and talks to it on stdio:

```bash
curu-memory serve
```

Do not run this command by hand. Give it to the MCP client, as in the [Quickstart](#quickstart). The server reads the index of the data directory. Messages for a person go to stderr. Set `CURU_MEMORY_PRINCIPAL` in the environment of the MCP client, so that the agent reads your personal and team spaces. To give the agent less than your access, see [Limit a local agent to one grant](#limit-a-local-agent-to-one-grant).

### Limit a local agent to one grant

A local agent on stdio reads as `CURU_MEMORY_PRINCIPAL`. To give it less, start it as the agent of one grant:

```bash
curu-memory serve --grant 3
```

The process then reads only the space of that grant. A grant that is not live stops the command with exit code 78. This limit is not a security boundary: a process can start with no `--grant`. With `--transport http`, `serve` refuses `--grant` with exit code 2, because the token of each request decides.

## Connect a remote agent

A remote agent connects over streamable HTTP. The server refuses each request without the token:

```bash
export CURU_MEMORY_HTTP_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
curu-memory serve --transport http --host 0.0.0.0 --port 6474
```

- The token must have 32 characters or more. Without it, the server does not start and exits with 78.
- `--host` sets the address. The default is `127.0.0.1`, which only this computer can reach.
- `--port` sets the port, from 1 to 65535. The default is 6474.
- A request without the token, or with a different token, gets HTTP 401 and no knowledge.

Then connect the agent with the same token:

```bash
claude mcp add --transport http curu-memory http://<host>:6474/mcp --header "Authorization: Bearer $CURU_MEMORY_HTTP_TOKEN"
```

The HTTP transport has no TLS. Put it behind a TLS proxy before you open it to a network that you do not control.

## Connect agents with OIDC tokens

Use OIDC tokens on HTTP, so that each person has their own identity. curu-memory checks each [access token](#glossary) with the keys of one [OIDC provider](#glossary). It never issues a token, and it keeps no password.

1. Register each person, and link the person to the [issuer](#glossary) and the [subject](#glossary) (`sub`) of their tokens. For a new person, use `add`:

   ```bash
   curu-memory access person add me --issuer https://auth.example.com/application/o/curu-memory/ --subject 4f0c2a
   ```

   For a person that exists, use `link`. A new link replaces the old link of the person:

   ```bash
   curu-memory access person link me --issuer https://auth.example.com/application/o/curu-memory/ --subject 4f0c2a
   ```

   Give `--issuer` the same text as `CURU_MEMORY_OIDC_ISSUER` and as the `iss` of the tokens, with the same last `/`.

2. Set the settings in the table below. Set `CURU_MEMORY_OIDC_RESOURCE` to the address that the MCP clients use, for example `https://memory.example.com/mcp`. Then start the server:

   ```bash
   curu-memory serve --transport http --host 0.0.0.0 --port 6474
   ```

3. Connect the MCP client with no token. The MCP client finds the provider from the metadata of the server, and asks the person to log in:

   ```bash
   claude mcp add --transport http curu-memory https://memory.example.com/mcp
   ```

| Setting | Meaning | Default |
|---|---|---|
| `CURU_MEMORY_OIDC_ISSUER` | The issuer URL of the provider, as in the `iss` of a token. It turns on the OIDC mode. Plain `http` is only for an issuer on `127.0.0.1`, `::1` or `localhost`. | none |
| `CURU_MEMORY_OIDC_RESOURCE` | The [canonical URI](#glossary) of the server: the address that the MCP clients use, with `/mcp`. | `http://<host>:<port>/mcp` of `serve` |
| `CURU_MEMORY_OIDC_AUDIENCE` | The value that the [audience](#glossary) (`aud`) of a token must hold. | the resource |
| `CURU_MEMORY_OIDC_TOKEN_TYPES` | The accepted `typ` values of the token header, separated by commas. | `at+jwt` |
| `CURU_MEMORY_OIDC_BASIC_CLIENT_ID` | The client identifier for the exchange of an app password. Empty turns the exchange off. See [Let an agent send its app password](#let-an-agent-send-its-app-password). | none |

A bad value stops each command with exit code 78.

- The server publishes its metadata at `/.well-known/oauth-protected-resource/mcp`. It names the provider.
- A request with no token, or with a refused token, gets HTTP 401. The `WWW-Authenticate` header names the metadata URL.
- curu-memory refuses a token with a bad signature, the algorithm `none` or an [HMAC](#glossary) algorithm.
- It also refuses a token of another issuer or audience. It refuses a token whose header has a `typ` that is not in the list. A token with no `typ` passes this check.
- It also refuses an expired token, and a token that is valid for more than 1 hour. It refuses a token that names no registered person and no registered agent.
- Each refused token writes a line to the audit log, with the reason and no part of the token.
- curu-memory reads the keys of the provider again for an unknown key, at most one time each 60 seconds. After a failed read, it waits 10 seconds.
- With `CURU_MEMORY_OIDC_ISSUER`, `serve` ignores `CURU_MEMORY_HTTP_TOKEN`, and writes a notice to stderr. The static token is the basic mode only when you use no OIDC provider. The browser has no OIDC login, so `browse` still uses the static token.
- A token of an agent, for example a token of the [client credentials grant](#glossary), reads only the spaces of its grants. See [Give an agent its own access](#give-an-agent-its-own-access). A token of no person and no agent gets HTTP 401.

### Use Authentik

Authentik puts the client identifier of its [OAuth client](#glossary) in `aud`, and writes the token type `JWT`. Make one provider only for curu-memory, so that no token of another application has that `aud`:

1. In Authentik, make an OAuth2/OpenID provider and an application for curu-memory. An Authentik [blueprint](#glossary) can also make them.
2. Select a [signing key](#glossary) for the provider, for example `authentik Self-signed Certificate`. Without a signing key, Authentik signs with HS256, and curu-memory refuses each token.
3. Add the [redirect URI](#glossary) of your MCP client to the provider.
4. Find the client identifier and the issuer of the provider. The issuer is `https://<authentik host>/application/o/<application slug>/`.
5. Set `CURU_MEMORY_OIDC_ISSUER` to the issuer, `CURU_MEMORY_OIDC_AUDIENCE` to the client identifier, and `CURU_MEMORY_OIDC_TOKEN_TYPES` to `at+jwt,JWT`.
6. Find your `sub`. The subject mode of the provider sets it. The default is a hash of your user identifier, which you can read in one of your tokens. With the subject mode "Based on the User's username", your `sub` is your user name.
7. Link your person to your `sub` with `curu-memory access person link`.

With the type `JWT`, or with no type, curu-memory also requires the claims `azp` and `scope`. An ID token of Authentik has no `scope`, so curu-memory refuses an ID token that an MCP client sends as an access token.

### Give an agent its own access

An agent acts for one person, and reads only the spaces of its grants. First, register the agent. Then give it a grant:

```bash
curu-memory access agent add scout --person me --issuer https://auth.example.com/application/o/curu-memory/ --subject 9b1e7d
curu-memory access grant scout --space team/look --role read --days 30
```

- Use `--subject` for an Authentik service account: its `sub` is in each of its tokens. Use `--client` for a rauthy client, whose tokens have no `sub`. A token with a `sub` is the agent of a client only when the `sub` is the client identifier. Thus a person that logs in through that client is not the agent.
- In a space, an agent gets the weaker of two roles: its strongest live grant, and the current role of its person. When the person loses a role, the agent loses it too.
- A grant lasts 90 days by default, and at most 366 days. The command refuses a grant that is stronger than the role of the person.
- `access grant` prints the identifier of the grant. End a grant with `curu-memory access revoke <id>`. From its next request, the agent reads that space only through its other live grants for that space, if it has some.
- `curu-memory access agent remove <name>` removes an agent and its grants. When you remove a person, its agents go too.
- `curu-memory access show` lists each agent and its live grants.
- An agent with no live grant gets the error `access_refused`. Each audit line of an agent names its person in the key `acts_for`.
- A token that matches more than one person or agent gets HTTP 401, with the audit reason `more than one principal`.

#### Let an agent send its app password

Many agent tools can send only a fixed header, and an Authentik token expires after some minutes. Set `CURU_MEMORY_OIDC_BASIC_CLIENT_ID` to the client identifier of the provider, for example `curu-memory`. Then an agent can send the user name and the app password of its Authentik service account, in the header `Authorization: Basic <base64 of user name:app password>`.

- curu-memory sends them to the token endpoint of the issuer, with the client credentials grant. Then it checks the token as each other token.
- curu-memory keeps the token until 30 seconds before its `exp` or its `expires_in`, whichever comes first, and at most 1 hour. It finds the kept token by the SHA-256 of the header, so it keeps no password.
- A refused exchange gets HTTP 401, and the audit log gets a line with the reason `basic exchange refused`. No line holds the user name or the password.
- curu-memory keeps a refused header for 10 seconds, so a repeat of it sends no request to the issuer.
- At most 4 exchanges run at the same time. More Basic requests wait, and bearer tokens do not wait for them.
- The token endpoint must use `https` off loopback.
- The app password goes to curu-memory with each request, so put TLS in front of curu-memory. Leave the setting empty where no agent needs it.

### Use the shipped rauthy

If you have no OIDC provider, use the rauthy of `deploy/rauthy/`. rauthy is an OIDC provider in one container, with no database server. Run these commands from the root of the repository:

```bash
deploy/rauthy/init.sh
docker compose -f deploy/rauthy/compose.yaml up -d
deploy/rauthy/setup.sh
```

- `init.sh` writes `deploy/rauthy/local/` with new random secrets, one time. Git ignores that directory.
- rauthy listens on `127.0.0.1:6480`, and its issuer is `http://localhost:6480/auth/v1/`.
- `setup.sh` lets the OAuth client `curu-memory` ask for tokens for `http://127.0.0.1:6474/mcp`. Set `CURU_MEMORY_OIDC_RESOURCE` before `init.sh` and `setup.sh` for another address.
- The OAuth client `curu-memory` uses the [authorization code grant](#glossary) with [PKCE](#glossary) (S256). Its redirect URIs are `http://localhost:6476/callback` and `http://127.0.0.1:6476/callback`. An MCP client that uses another redirect URI registers its own OAuth client.
- The first start writes the password of `admin@localhost` to the log of the container. Read it with `docker compose -f deploy/rauthy/compose.yaml logs rauthy`.
- Make your user in the admin pages of rauthy. A user needs a given name. Your `sub` is the user identifier that rauthy shows.
- An access token of the OAuth client `curu-memory` is valid for 10 minutes.

See [deploy/rauthy/README.md](deploy/rauthy/README.md) for a server on a network, behind a TLS proxy.

## Read a concept and follow its links

The four tools are read-only:

| Tool | Input | Result |
|---|---|---|
| `search_knowledge` | `question`, and optional `limit`, `types` and `tags` | The matching concepts, the `mode` and the `notice`, as in [Search the knowledge](#search-the-knowledge). |
| `search_knowledge_by_media` | `media` as base64 text or a base64 `data:` URI, and optional `limit`, `types` and `tags` | The same object as `search_knowledge`, with the `mode` `media`. See [Search with media](#search-with-media). |
| `get_concept` | `id` | The fields of one concept, its space, its path, its full Markdown body and its `media` list. Each `media` item gives the `path` relative to the concept directory, the [`sha256`](#glossary) and the [`mime`](#glossary) type of an image, a clip or a video. |
| `concept_neighbours` | `id` | The links from the concept (`outgoing`) and to the concept (`incoming`). |

Each neighbour gives the identifier, the link kind and the title of the other concept. `resolved` is false for a link to a concept that is not in a space that the caller can read. Such a link has no title. Use these tools in sequence: search, read the best concept, then follow its links for more context.

## Browse the knowledge

A person can search the knowledge and follow the links of a concept in a web browser:

```bash
curu-memory browse
```

- `--host` sets the address. The default is `127.0.0.1`, which only this computer can reach.
- `--port` sets the port, from 1 to 65535. The default is 6475.

Open `http://127.0.0.1:6475/`. Type a question, and the page lists the same concepts, in the same order, as the `search_knowledge` tool. Each result shows its title, type, description, tags and score. The score, the mode and the notice mean the same as in [Search the knowledge](#search-the-knowledge). Select a result to open its page.

- The page of a concept shows its fields, its sources, its body and its links in both directions.
- The page of a concept also shows each image and plays each clip and video of its `media` list that is in the index. The caption is the path relative to the concept directory. The index holds a copy of each file, so the browser does not need the concept directory. Run the index command again to show a changed file.
- A link to another concept in the index opens the page of that concept.
- A link to a missing concept is crossed out and has no address.

Without an index, each page says "No index" and gets HTTP 503. Run `curu-memory index`, then load the page again.

The browser only reads. It accepts GET and HEAD only, and each other method gets HTTP 405. Each media file has the address `/media/<sha256>`, from the SHA-256 of its bytes. The browser answers a Range request, so a web browser can seek in a clip. Each other address under `/media/` gets HTTP 404. The browser never reads a file path from a request. HTML and scripts in a concept show as text and never run.

To open the browser to other computers, set the token first:

```bash
export CURU_MEMORY_HTTP_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
curu-memory browse --host 0.0.0.0
```

The web browser then asks for a user name and a password. Type any user name, and the token as the password. Without a token of 32 characters or more, the command does not start and exits with 78.

## Handle errors

A tool failure is an error result. Its text is one JSON object with `code`, `message` and `details`:

```json
{"code": "concept_not_found", "message": "No concept has the identifier 'x'. Use an identifier from search_knowledge.", "details": {"id": "x"}}
```

| Code | Cause | What to do |
|---|---|---|
| `index_missing` | The data directory has no index. | Run `curu-memory index`. |
| `concept_not_found` | No concept has the identifier. | Use an identifier from `search_knowledge`. |
| `access_refused` | The principal can read no space, for example a person that the operator removed, or an agent with no live grant. | Ask the operator to register the person, or to give the agent a grant. |
| `invalid_argument` | A value does not obey a rule, for example a question of only spaces. `details.field` names the field. | Correct the value. |

The tool schema refuses some values first, for example `limit: 51` or an empty question. For these values, the MCP SDK gives its own validation error, not one of these codes.

The `curu-memory` command uses these exit codes:

| Exit code | Meaning |
|---|---|
| 0 | Success. |
| 1 | `index` skipped a file, the embedder failed, another index run held the lock, or the embedder model was not the model of another space. `search` found no index, or no readable space. `access` refused a change. |
| 2 | A usage error, for example `--limit 51`. |
| 78 | A configuration error, for example HTTP with no token. |

## Limits

- On stdio, in the browser and with the static token, one process reads as one person (`CURU_MEMORY_PRINCIPAL`). On stdio, `serve --grant` makes the process read as the agent of one grant. Only the OIDC mode of HTTP gives each request its own person or agent.
- There is no encryption at rest yet.
- The browser has no OIDC login.
- Agents and persons cannot write knowledge through curu-memory yet. Edit the concept files, then run the index command.
- The browser accepts one static token. Without `CURU_MEMORY_OIDC_ISSUER`, the HTTP transport also accepts only one static token. Neither has TLS.
- The browser has no log-out button. Close the web browser to forget the token.
- The browser shows the links of a concept as lists, not as a picture of the graph.
- Search compares each embedding with the question one by one. This is fast for some thousands of concepts. It becomes slow at about 100,000 concepts.
- A match of meaning needs a [cosine similarity](#glossary) of 0.68 or more with EmbeddingGemma 2. The measured margin is small: unrelated questions reached 0.67, and known answers started at 0.70. A real question with few shared words can miss its answer by meaning. With another model, the limit is 0.35. Some models give lower values for good matches, so measure the limit and set `CURU_MEMORY_EMBEDDER_MIN_SIMILARITY`.
- A match between a question in words and an image also needs 0.68 or more with EmbeddingGemma 2. Simple test images scored from 0.74 with their descriptions, and wrong images reached 0.66. Real outputs of ComfyUI can score lower. If an image that you describe well does not match, measure and set `CURU_MEMORY_EMBEDDER_MIN_IMAGE_SIMILARITY`.
- A search with an image uses 0.89 for image to image and 0.70 for image to text with EmbeddingGemma 2. The margin for image to text is small: a wrong description of a test image reached 0.695, and an unrelated curu concept reached 0.684.
- A search with a clip uses 0.87 for clip to clip and 0.70 for clip to text. The margin for clip to text is also small: a wrong description reached 0.692. A correct description scored as low as 0.712 in an earlier measurement. These limits come from changed copies of speech clips. Music, sound effects and other recordings can score lower.
- A search with a video uses 0.92 for video to video and 0.73 for video to text. The margin for video to text is the smallest. A wrong description reached 0.728 in an earlier measurement, and a correct description scored as low as 0.737. These limits come from changed copies of short animations. Real outputs of ComfyUI can score lower.
- A question cannot hold words and media together. The browser cannot search with media. The list of search results does not show media. Each kind of media question finds only its own kind of media and texts.
- A match between a question in words and a video needs 0.71 or more with EmbeddingGemma 2. The test videos are short animations of shapes. A question about motion can match a video with a near motion.
- A match between a question in words and a clip needs 0.68 or more with EmbeddingGemma 2. The limit comes from speech clips. Speech test clips scored from 0.73 with their own questions. Wrong clips and unrelated questions reached 0.64. Music and sound effects can score lower.
- The index holds a copy of each media file. One hundred files of 2 MB make the index about 200 MB larger.
- There is no file watcher. Run the index command after each change.
- curu-memory has no public Python API yet. Each module is internal.

## Technical notes

- The index is one SQLite file for each space in the data directory, for example `~/.local/share/curu-memory/global.sqlite`. The file of `team/look` is `team--look.sqlite`. `CURU_MEMORY_DATA_DIR` changes the directory. `XDG_DATA_HOME` also changes the default. See [ADR-001](project-management/ADRs/ADR-001-sqlite-index-per-tier-space.md). The space names and the readable spaces come from [ADR-016](project-management/ADRs/ADR-016-spaces-roles-and-agent-grants.md).
- The index lock uses `fcntl.flock`, so curu-memory runs on Linux and macOS, not on Windows.
- The OIDC mode follows [ADR-015](project-management/ADRs/ADR-015-oidc-resource-server.md) and [ADR-017](project-management/ADRs/ADR-017-oidc-settings-for-authentik.md). The contract is [specs/012-oidc-tokens-with-the-shipped-rauthy/contracts/oidc.md](specs/012-oidc-tokens-with-the-shipped-rauthy/contracts/oidc.md).
- Agents, grants and the Basic exchange follow [ADR-018](project-management/ADRs/ADR-018-agents-grants-and-the-basic-exchange.md). The contract is [specs/013-agent-grants/contracts/agents.md](specs/013-agent-grants/contracts/agents.md). The exchange sends `grant_type=client_credentials`, `client_id`, `username`, `password` and `scope=openid profile`. The cache keeps at most 1,000 tokens, and it removes the oldest first.
- curu-memory accepts keys of the algorithms RS256, RS384, RS512, PS256, ES256 and EdDSA. curu-memory refuses each token that a key of another algorithm signs, for example ES384.
- curu-memory reads the keys from `<issuer>/.well-known/openid-configuration` and its `jwks_uri`. The document must name the same issuer, and `jwks_uri` must use `https` off loopback. The clock skew of the token check is 60 seconds.
- The browser makes its pages on the server, with no JavaScript. A [content security policy](#glossary) blocks each script. See [ADR-004](project-management/ADRs/ADR-004-server-rendered-read-only-browser.md).
- The limit of the match of meaning comes from a measurement of EmbeddingGemma 2. See [ADR-005](project-management/ADRs/ADR-005-similarity-limit-for-each-model.md). The limit for images has its own measurement. See [ADR-008](project-management/ADRs/ADR-008-image-similarity-limit.md). The limits for audio and for video also have their own measurements. See [ADR-011](project-management/ADRs/ADR-011-audio-in-concepts.md) and [ADR-013](project-management/ADRs/ADR-013-video-in-concepts.md). A search with media has its own limits and its own measurement, and one tool takes all media. See [ADR-010](project-management/ADRs/ADR-010-search-with-an-image.md), [ADR-012](project-management/ADRs/ADR-012-one-tool-for-a-media-question.md) and [ADR-014](project-management/ADRs/ADR-014-limits-of-a-video-question.md).
- curu-memory sends each image to the embedder in an item `{"image": "data:..."}`, each clip in an item `{"audio": "data:..."}` and each video in an item `{"video": "data:..."}`. The value is a `data:` URI: a text that holds the file bytes in base64, with their MIME type. See [ADR-007](project-management/ADRs/ADR-007-media-references-and-transport.md). The index keeps the bytes of each media file one time for each SHA-256. See [ADR-009](project-management/ADRs/ADR-009-media-bytes-in-the-index.md).
- The plan for keys and encryption of private knowledge is [ADR-002](project-management/ADRs/ADR-002-server-held-envelope-keys.md).
- The word match uses SQLite FTS5 (the full-text search of SQLite) with the `unicode61` tokenizer (the rule that splits a text into words). The tokenizer keeps `.`, `_` and `-` in a word. A word of three or more characters also matches as a prefix.
- Search fuses the two rankings with reciprocal rank fusion, with k = 60. The lower of the two values counts one tenth. So ten rivals that are a little worse in both rankings do not push out the best answer of one ranking. See [ADR-006](project-management/ADRs/ADR-006-damped-rank-fusion.md). The identifier breaks a tie, so the order is stable.
- The concept rules are the same as the rules of the curu index, so each curu concept file indexes here.
- The tool contracts are in [specs/001-okf-index-mcp-search/contracts/](specs/001-okf-index-mcp-search/contracts/).
- curu-memory uses the official MCP Python SDK. It never loads model weights.
- curu uses curu-memory as a library, and the version range of a release follows a version policy. See [ADR-019](project-management/ADRs/ADR-019-curu-uses-curu-memory-as-a-library.md).

## Develop

```bash
git clone https://github.com/darth-veitcher/curu-memory
cd curu-memory
uv sync
uv run pytest
```

The default tests need no network, no GPU and no model. They use a fake embedder and a copy of the curu concept files. The tests with a real embedder have the `integration` marker. With no GPU embedder server, start the [development embedder](#glossary) first, as [tools/embedder_server/README.md](tools/embedder_server/README.md) says. Then run:

```bash
CURU_MEMORY_EMBEDDER_URL=http://127.0.0.1:30000 CURU_MEMORY_EMBEDDER_TIMEOUT=300 uv run pytest -m integration
```

The rauthy test of the integration tier also needs Docker and a free port 6480.

Before a pull request, run the gates:

```bash
uv run ruff check --fix && uv run ruff format
uv run ty check
beacon doctor --strict
```

The CI runs the same gates on each pull request and on each push to `main`. A release starts when the owner pushes a tag `vX.Y.Z` on `main`. The release workflow checks the tag, then publishes the package to PyPI.

`AGENTS.md` gives the rules for contributors and agents.

## Glossary

- **Agent**: a program that uses a language model and calls tools, for example Claude Code. A registered agent acts for one person, and reads only the spaces of its grants.
- **Concept**: one OKF file about one topic, for example a model, a node or a finding.
- **Clip**: an audio file: an item of the `media` list of a concept, or the file that you search with.
- **Base64**: a text form of bytes that uses only letters, digits, `+`, `/` and `=`.
- **Browser**: the read-only web pages that `curu-memory browse` serves.
- **Video**: a video file: an item of the `media` list of a concept, or the file that you search with. The formats are MP4, MOV, WebM and MKV.
- **CI**: the GitHub workflow that runs the quality gates on each pull request and each push to `main`.
- **Content security policy**: an HTTP header that tells the web browser which scripts, styles, images, audio and video it can load.
- **Cosine similarity**: a value from -1 to 1 that tells how near two embeddings are. A higher value is a nearer meaning.
- **Data URI**: a text that holds the bytes of a file in base64, with their MIME type, for example `data:image/png;base64,...`.
- **Data directory**: the directory of the index, the embedding cache, the access store `access.sqlite` and the audit log `audit.jsonl`. The default is `~/.local/share/curu-memory`, or `$XDG_DATA_HOME/curu-memory`. `CURU_MEMORY_DATA_DIR` changes it.
- **Development embedder**: the CPU server in `tools/embedder_server/` that serves EmbeddingGemma 2 for the integration tier.
- **Embedder**: a server that changes a text, an image, a clip or a video into an embedding.
- **Embedding**: a list of numbers that holds the meaning of a text, an image, a clip or a video. Items with a near meaning have near embeddings.
- **GraphRAG**: retrieval that uses a search and the links between concepts to give an agent its context.
- **Index**: the searchable copy of the concept files that the index command makes.
- **Link kind**: the type of a link between two concepts, for example `USES_COMPONENT`.
- **MCP**: the Model Context Protocol, which lets an agent find and call tools.
- **Minor version**: the second number of `X.Y.Z`. Below 1.0, a new minor version can remove or change a name or a tool field.
- **OKF**: Open Knowledge Format, the format of the concept files: YAML front matter and a Markdown body.
- **MIME type**: the name of a file format, for example `image/png`.
- **PyPI**: the Python Package Index, the public server of Python packages.
- **Reciprocal rank fusion**: a method that joins two rankings. Each ranking gives 1 / (60 + rank) to each concept. The best of the two values counts in full, and the other counts one tenth.
- **Task prompt**: a fixed text that curu-memory puts around a question or a section before it sends the text to the embedder.
- **Stdio**: the standard input and output of a process. A local MCP client talks to the server through them.
- **Streamable HTTP**: the MCP transport over HTTP, for remote agents.
- **Web browser**: the program that shows web pages, for example Firefox.
- **SHA-256**: a hash of 64 hexadecimal characters that the bytes of a file give. Different bytes give a different SHA-256.
- **Unit**: one part of a concept that has its own embedding: the summary, the overview, a `##` section, an image, a clip or a video.
- **Version range**: the lowest version and the first version to exclude, for example `>=0.1,<0.2`.
- **Space**: a part of the knowledge with its own index: `global`, `team/<slug>` or `personal/<handle>`. Each request reads only the spaces that its caller can read.
- **Team slug**: the short name of a team in a space name, for example `look` in `team/look`.
- **Instance owner**: a person with `admin` on `global` and on each team space. The first registered person is an instance owner.
- **Principal**: the person or the agent of a request: from `CURU_MEMORY_PRINCIPAL`, from `serve --grant`, from the OIDC token on HTTP, or `anonymous` with no principal.
- **Operator**: the person who runs `curu-memory access` and `curu-memory index` on the computer of the server.
- **Access store**: `access.sqlite`, with the persons, the teams, the members, the agents and the grants.
- **Audit log**: `audit.jsonl`, with one line for each change of access, each refused read and each refused token.
- **OIDC provider**: a service that logs persons in and issues tokens, for example Authentik or rauthy. OIDC is OpenID Connect.
- **Issuer**: the URL of an OIDC provider, as in the `iss` claim of its tokens.
- **Subject**: the identifier of a person or a service account at an issuer, as in the `sub` claim of a token.
- **Audience**: the `aud` claim of a token: the service or the OAuth client that the token is for.
- **Access token**: a signed JWT (JSON Web Token) that a client sends in the `Authorization` header. A claim is one field of a token.
- **ID token**: a token that tells a client who logged in. It is not for a service such as curu-memory.
- **OAuth client**: a program that the provider knows by its client identifier. An MCP client uses an OAuth client.
- **Signing key**: the key that signs the tokens of a provider. curu-memory checks each signature with the public part.
- **HMAC**: signatures with a shared secret, for example HS256. A service cannot check them without the secret, so curu-memory refuses them.
- **Canonical URI**: the one address of the server that tokens name, for example `https://memory.example.com/mcp`.
- **OAuth grant**: the method that a client uses to get a token, for example the client credentials grant.
- **Grant**: one role of one agent in one space, for 1 to 366 days. A grant is live from its start until it expires or the operator revokes it.
- **Service account**: an Authentik user for a program, not for a person.
- **App password**: a password that Authentik makes for one user, for a program.
- **Token endpoint**: the URL of the issuer that gives tokens. The discovery document of the issuer names it.
- **Basic header**: `Authorization: Basic` followed by the base64 of `user name:password`.
- **Authorization code grant**: the person logs in through the web browser, and the client gets a token for that person.
- **PKCE**: a check that only the client that started a login can get its token.
- **Client credentials grant**: a program gets a token with its own secret, with no person.
- **Blueprint**: a YAML file that Authentik reads to make its objects, for example a provider and an application.
- **Redirect URI**: the address where the provider sends the person back after the login.
- **Role**: `read`, `write` or `admin` of one person or one agent in one space.
- **Handle**: the short name of a person in a space name, for example `me` in `personal/me`.
