Skip to main content

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

Install

You need Python 3.12 or later and uv.

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:

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:

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 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:

---
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:

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:

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 is the short name of the team, and the handle 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. 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 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:

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
  • --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, 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_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.
  • 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 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_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 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 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.
  • --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:

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.

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

    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 --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:

    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:

    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-Authenticate header names the metadata URL.
  • curu-memory refuses a token with a bad signature, the algorithm none or an HMAC 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, 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:

  1. In Authentik, make an OAuth2/OpenID provider and an application for curu-memory. An Authentik blueprint can also make them.
  2. 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.
  3. Add the redirect URI 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:

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:

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 with PKCE (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 for a server on a network, behind a TLS proxy.

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
  • --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. 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:

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.

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 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. 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, 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 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 a data: 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 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. 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.

Develop

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 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 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.

Metadata

Release files for curu-memory 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for curu-memory 0.2.0
File Size Uploaded Provenance Actions
curu_memory-0.2.0.tar.gz 2.8 MB With provenance Details for curu_memory-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for curu-memory 0.2.0
File Interpreter ABI Platform Actions
curu_memory-0.2.0-py3-none-any.whl Python 3 none any Details for curu_memory-0.2.0-py3-none-any.whl

Total release size: 2.9 MB

Release files / curu_memory-0.2.0.tar.gz

Download URL curu_memory-0.2.0.tar.gz
Size 2.8 MB
Tags Source
SHA-256 checksum
How to use checksums
0ed64c0c7eb3f6c5962dd5c128bc70be7a63cd1cb85144892e31f91b16d2e92c
BLAKE2b-256 checksum
How to use checksums
8f763c361b8f5f24af53b7a08aec471c8865976998ba2b1bc2d1e14d0bf2081e
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 log

Release files / curu_memory-0.2.0-py3-none-any.whl

Download URL curu_memory-0.2.0-py3-none-any.whl
Size 88.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1a218f1e253dac287adf841bc9843598e222b6232d728bec1feae147714d7286
BLAKE2b-256 checksum
How to use checksums
6db7ab4dcc5bf98d71f8114f5bf3605f11f41c9dd62c1813d4cfa63b31cb69b2
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 log

Security for curu-memory 0.2.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.

Select from 2 files

Attestation for curu_memory-0.2.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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page