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 |
| Add images, audio and video to a concept | Add a media: list to the front matter |
Add media to a concept |
| Build the index from your concept files | curu-memory index --concepts ./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 |
| Give each person their own spaces, and see who reads which space | curu-memory access person add me |
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 |
| 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 |
| Search the knowledge from a terminal | curu-memory search "VAE for WAN2.2 5B" |
Search the knowledge |
| Give the knowledge to a local agent | curu-memory serve |
Connect a local agent |
| Give the knowledge to a remote agent | curu-memory serve --transport http |
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 by meaning with EmbeddingGemma 2 | Set CURU_MEMORY_EMBEDDER_URL |
Search by meaning |
| Browse the knowledge in a web browser | curu-memory browse |
Browse the knowledge |
| Read a concept and its links | The get_concept and concept_neighbours tools |
Read a concept and follow its links |
| Find why a call failed | The code of the error |
Handle errors |
| Build and read the knowledge from a Python program | curu_memory.open_knowledge() |
Use curu-memory from Python |
Install
You need Python 3.12 or later and uv.
uv tool install "curu-memory[server] @ git+https://github.com/darth-veitcher/curu-memory"
This installs the curu-memory command with the server extra. serve and browse need the extra. Without it, index, search and access work, and serve and browse stop with exit code 78.
To install a release from PyPI, give a version range:
uv tool install "curu-memory[server]>=0.3,<0.4"
A program that uses curu-memory as a library pins a range in its pyproject.toml, with no extra. 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:
mkdir -p knowledge
Save this text as knowledge/wan22-5b-vae.md:
---
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:
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:
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 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 iswan22-5b-vae. - A missing
titlebecomes the identifier. A missingtypebecomesUntyped, and the index report gives a warning. - A link to another concept is a Markdown link to its
.mdfile. 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_FROMandRETRACTS_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:
---
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
mediakey, so the same concept files still index in curu.
Index your knowledge
Run the index command after each change to the concept files:
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
mediavalue is not a list of paths. The command ignores the full list, andmedia skippeddoes 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:
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>orpersonal/<handle>. The default isglobal. - The team slug is the short name of the team, and the handle is the short name of the person. Each has 1 to 40 characters from
atoz,0to9and-. 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. 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:
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 reads only the spaces where it has a role. 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 readsglobalonly. - A
CURU_MEMORY_PRINCIPALthat names no registered person stopsserve,browseandsearchwith 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 --ownerregisters another instance owner. The command refuses to remove the last instance owner.curu-memory access person remove,access team removeandaccess member removeremove 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.sqliteorteam--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:
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
curu-memory search "VAE for WAN2.2 5B" --limit 5
curu-memory search "turbo" --type "Model Component" --tag wan
--limitsets the number of results, from 1 to 50. The default is 10.--typekeeps only concepts of that OKF type. Repeat it for more types.--tagkeeps only concepts that have the tag. Repeat it, and each concept must have all the tags.--jsonprints the same JSON object as thesearch_knowledgetool.
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, 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:
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_kindsays which kind matched. - Media has no words, so only the meaning counts. The
modeismedia. Thescoreis 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, anddetails.fieldismedia. See Handle errors. - The search needs an embedder that accepts the kind of the file. Without one, the result is empty, and the
noticesays why. --limit,--type,--tagand--jsonwork as for a question in words.
An agent uses the tool search_knowledge_by_media. It gives the file as base64 text or as a base64 data URI, 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 that serves POST /v1/embeddings. SGLang, vLLM, Ollama and llama-server serve this API. The default model is 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:
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:
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:
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_URLis the base URL of the embedder. A URL that ends with/v1also works.CURU_MEMORY_EMBEDDER_MODELis the model name that the embedder serves. The default isgoogle/embeddinggemma-2. Without a URL, this setting stops each command with exit code 78.CURU_MEMORY_EMBEDDER_TOKENis an optional bearer token for the embedder.CURU_MEMORY_EMBEDDER_TIMEOUTis 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_SIMILARITYis the lowest cosine similarity 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_SIMILARITYis 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_SIMILARITYis 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_SIMILARITYis 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_SIMILARITYis 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_SIMILARITYis 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_SIMILARITYis 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_SIMILARITYis 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_SIMILARITYis 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_SIMILARITYis 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 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:
curu-memory serve
Do not run this command by hand. Give it to the MCP client, as in the 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
A local agent on stdio reads as CURU_MEMORY_PRINCIPAL. To give it less, start it as the agent of one grant:
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:
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.
--hostsets the address. The default is127.0.0.1, which only this computer can reach.--portsets 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:
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 with the keys of one OIDC provider. It never issues a token, and it keeps no password.
-
Register each person, and link the person to the issuer and the subject (
sub) of their tokens. For a new person, useadd: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:curu-memory access person link me --issuer https://auth.example.com/application/o/curu-memory/ --subject 4f0c2a
Give
--issuerthe same text asCURU_MEMORY_OIDC_ISSUERand as theissof the tokens, with the same last/. -
Set the settings in the table below. Set
CURU_MEMORY_OIDC_RESOURCEto the address that the MCP clients use, for examplehttps://memory.example.com/mcp. Then start the server:curu-memory serve --transport http --host 0.0.0.0 --port 6474
-
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:
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 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 (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. | 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-Authenticateheader names the metadata URL. - curu-memory refuses a token with a bad signature, the algorithm
noneor an HMAC algorithm. - It also refuses a token of another issuer or audience. It refuses a token whose header has a
typthat is not in the list. A token with notyppasses 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,serveignoresCURU_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, sobrowsestill uses the static token. - A token of an agent, for example a token of the client credentials grant, reads only the spaces of its grants. See 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 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:
- In Authentik, make an OAuth2/OpenID provider and an application for curu-memory. An Authentik blueprint can also make them.
- Select a signing key for the provider, for example
authentik Self-signed Certificate. Without a signing key, Authentik signs with HS256, and curu-memory refuses each token. - Add the redirect URI of your MCP client to the provider.
- Find the client identifier and the issuer of the provider. The issuer is
https://<authentik host>/application/o/<application slug>/. - Set
CURU_MEMORY_OIDC_ISSUERto the issuer,CURU_MEMORY_OIDC_AUDIENCEto the client identifier, andCURU_MEMORY_OIDC_TOKEN_TYPEStoat+jwt,JWT. - 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", yoursubis your user name. - Link your person to your
subwithcuru-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:
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
--subjectfor an Authentik service account: itssubis in each of its tokens. Use--clientfor a rauthy client, whose tokens have nosub. A token with asubis the agent of a client only when thesubis 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 grantprints the identifier of the grant. End a grant withcuru-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 showlists 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 keyacts_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
expor itsexpires_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
httpsoff 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:
deploy/rauthy/init.sh
docker compose -f deploy/rauthy/compose.yaml up -d
deploy/rauthy/setup.sh
init.shwritesdeploy/rauthy/local/with new random secrets, one time. Git ignores that directory.- rauthy listens on
127.0.0.1:6480, and its issuer ishttp://localhost:6480/auth/v1/. setup.shlets the OAuth clientcuru-memoryask for tokens forhttp://127.0.0.1:6474/mcp. SetCURU_MEMORY_OIDC_RESOURCEbeforeinit.shandsetup.shfor another address.- The OAuth client
curu-memoryuses the authorization code grant with PKCE (S256). Its redirect URIs arehttp://localhost:6476/callbackandhttp://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@localhostto the log of the container. Read it withdocker compose -f deploy/rauthy/compose.yaml logs rauthy. - Make your user in the admin pages of rauthy. A user needs a given name. Your
subis the user identifier that rauthy shows. - An access token of the OAuth client
curu-memoryis valid for 10 minutes.
See 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_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. |
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 and the mime 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:
curu-memory browse
--hostsets the address. The default is127.0.0.1, which only this computer can reach.--portsets 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. 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
medialist 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:
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:
{"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, or serve with no server extra. |
Use curu-memory from Python
Use the library to build a space and to read it from a Python program, with no server. Import each name from curu_memory only. The names of curu_memory.__all__ are the public API. Each other name can change in any release. The public API starts with version 0.3.0, so a program pins curu-memory>=0.3,<0.4.
from pathlib import Path
import curu_memory
report = curu_memory.build_space(Path("knowledge"))
print(report.concepts_indexed, len(report.skipped))
knowledge = curu_memory.open_knowledge()
for hit in knowledge.search("VAE for WAN2.2 5B", limit=3).results:
print(hit.id, hit.space)
concept = knowledge.get_concept("wan22-5b-vae")
links = knowledge.neighbours(concept.id)
build_spacebuilds one space, as the index command does.space=names the space, and the default isglobal. A skipped file is not an error. The fields of the report are the values of Index your knowledge, exceptindex, for exampleconcepts_indexed.skippedholds a (path, reason) pair for each skipped file.open_knowledgereads as one principal. Giveperson="<handle>"to read the spaces of that person. Givegrant=<id>to read only the space of that grant. With neither, the library readsglobalonly.- An unknown, ended or revoked grant does not stop
open_knowledge. Each read then raisesNoReadableSpace.serve --grantstops at the start for such a grant. - Each read gets the readable spaces again. A removed role or a revoked grant stops the next read.
search,get_conceptandneighboursgive the same results as the toolssearch_knowledge,get_conceptandconcept_neighbours. Each hit gives itsspace.- Each refused read writes the line in the audit log that the server writes, with the name of the matching tool as the action.
data_dir=replaces the data directory.env=gives theCURU_MEMORY_*variables as a dictionary, for example the embedder settings of Search by meaning. With noenv, the library reads the environment of the process.- The library does not use
CURU_MEMORY_PRINCIPALto select the principal. A value that is not a valid handle raisesConfigError. - The principal is not a security boundary in one process. The boundary is who can read the data directory.
Each error is in curu_memory:
| Call | Cause | Error |
|---|---|---|
build_space |
The concept directory does not exist, or the space name is not valid. details.field names the argument. |
InvalidArgument |
build_space |
Another index run holds the lock. | IndexLocked |
build_space |
The embedder model is not the model of another space. | IndexRefused |
build_space |
The embedder failed. The old index stays. | EmbedderFailed |
build_space, open_knowledge |
A CURU_MEMORY_* variable is not valid. |
ConfigError |
open_knowledge |
No person has the handle, or person and grant are both given. |
InvalidArgument |
| each read | The principal can read no space. | NoReadableSpace |
get_concept, neighbours |
No readable space holds the concept. | ConceptNotFound |
search |
The question is empty or too long, or the limit is not from 1 to 50. | InvalidArgument |
| each read | No readable space has an index. | IndexMissing |
Each error except IndexLocked and ConfigError is a CuruMemoryError. Its code is the code that a tool gives, for example concept_not_found.
Limits
- On stdio, in the browser and with the static token, one process reads as one person (
CURU_MEMORY_PRINCIPAL). In Python, eachKnowledgereads as the principal of itsopen_knowledge. On stdio,serve --grantmakes 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 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.
- The Python API cannot search with media, and it cannot write a concept. Use
curu-memory search --mediaor the toolsearch_knowledge_by_mediafor media.
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 ofteam/lookisteam--look.sqlite.CURU_MEMORY_DATA_DIRchanges the directory.XDG_DATA_HOMEalso changes the default. See ADR-001. The space names and the readable spaces come from ADR-016. - The index lock uses
fcntl.flock, so curu-memory runs on Linux and macOS, not on Windows. - The OIDC mode follows ADR-015 and ADR-017. The contract is specs/012-oidc-tokens-with-the-shipped-rauthy/contracts/oidc.md.
- Agents, grants and the Basic exchange follow ADR-018. The contract is specs/013-agent-grants/contracts/agents.md. The exchange sends
grant_type=client_credentials,client_id,username,passwordandscope=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-configurationand itsjwks_uri. The document must name the same issuer, andjwks_urimust usehttpsoff 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 blocks each script. See ADR-004.
- The limit of the match of meaning comes from a measurement of EmbeddingGemma 2. See ADR-005. The limit for images has its own measurement. See ADR-008. The limits for audio and for video also have their own measurements. See ADR-011 and ADR-013. A search with media has its own limits and its own measurement, and one tool takes all media. See ADR-010, ADR-012 and ADR-014.
- 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 adata:URI: a text that holds the file bytes in base64, with their MIME type. See ADR-007. The index keeps the bytes of each media file one time for each SHA-256. See ADR-009. - The plan for keys and encryption of private knowledge is ADR-002.
- The word match uses SQLite FTS5 (the full-text search of SQLite) with the
unicode61tokenizer (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. 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/.
- 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.
tests/contract/public_api.txtholds the names ofcuru_memory.__all__. The contract is specs/015-the-public-library-api/contracts/public-api.md. - The core install has
pydantic,httpx2,markdown-it-pyandpyyaml. Theserverextra addsmcp,starletteanduvicorn.
Develop
git clone https://github.com/darth-veitcher/curu-memory
cd curu-memory
uv sync
uv run pytest
uv sync installs the server extra, because the development group names it. 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 first, as tools/embedder_server/README.md says. Then run:
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:
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
medialist 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 browseserves. - Video: a video file: an item of the
medialist 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.sqliteand the audit logaudit.jsonl. The default is~/.local/share/curu-memory, or$XDG_DATA_HOME/curu-memory.CURU_MEMORY_DATA_DIRchanges it. - Development embedder: the CPU server in
tools/embedder_server/that serves EmbeddingGemma 2 for the integration tier. - Extra: a named set of optional packages of a Python package, for example
serverincuru-memory[server]. - 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.3,<0.4. - Space: a part of the knowledge with its own index:
global,team/<slug>orpersonal/<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
lookinteam/look. - Instance owner: a person with
adminonglobaland on each team space. The first registered person is an instance owner. - Principal: the person or the agent of a request, or
anonymouswith no principal. It comes fromCURU_MEMORY_PRINCIPAL,serve --grant, the OIDC token on HTTP oropen_knowledge. - Operator: the person who runs
curu-memory accessandcuru-memory indexon 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
issclaim of its tokens. - Subject: the identifier of a person or a service account at an issuer, as in the
subclaim of a token. - Audience: the
audclaim 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
Authorizationheader. 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: Basicfollowed by the base64 ofuser 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,writeoradminof one person or one agent in one space. - Handle: the short name of a person in a space name, for example
meinpersonal/me.
Metadata
Release files for curu-memory 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| curu_memory-0.3.0.tar.gz | 2.8 MB | With provenance | Details for curu_memory-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| curu_memory-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for curu_memory-0.3.0-py3-none-any.whl |
Total release size: 2.9 MB
Release files / curu_memory-0.3.0.tar.gz
| Download URL | curu_memory-0.3.0.tar.gz |
|---|---|
| Size | 2.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a6e83655eb192359769b035a31fae163eb09c56ba3aa3e96e8ef15ac9a07c6e6
|
|
BLAKE2b-256 checksum How to use checksums |
106a9cea749ad8df86bfbb728a46db9e1217dc7f0c9626d7e3ce160cbf989b3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logRelease files / curu_memory-0.3.0-py3-none-any.whl
| Download URL | curu_memory-0.3.0-py3-none-any.whl |
|---|---|
| Size | 93.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4ce146120c33e1af4115c346b2c997792d38c5cd77a959ffd03f8cb46eaca819
|
|
BLAKE2b-256 checksum How to use checksums |
585ad3d5ee197384abca97e9e60ec2a477dcff780532fb784cf1a01d09b4083d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logSecurity for curu-memory 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for curu_memory-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.