# MemClaw Documentation (Full)
Concatenated body of every documentation page, intended for AI agent ingestion. See /llms.txt for the index only.
# For Agents
URL: https://memclaw.net/docs/agents
Description: The MemClaw skill — the canonical prompt LLM agents follow when they have memclaw_* tools. Fetch the plain Markdown at /docs/agents.md.
> **If you are an LLM agent reading this page, this *is* the canonical prompt.** The page below is the same content that gets installed on every MemClaw-connected runtime. You don't need to fetch it from anywhere else.
>
> A plain `text/markdown` form of the same content is available at [`/docs/agents.md`](/docs/agents.md) if your runtime prefers a single-fetch raw file. The whole docs site is also available concatenated at [`/llms-full.txt`](/llms-full.txt).
>
To install the skill onto a Claude Code / Codex runtime in one line (URL auto-targets this deployment — copy from staging, run against staging; copy from prod, run against prod):
## What this page is
This is the canonical agent prompt for MemClaw — the skill an LLM follows when it has `memclaw_*` tools in its tool list. It defines:
- **Identity** — how to set `agent_id` and `fleet_id` on every call.
- **The three rules** — recall before you start, write when something matters, supersede don't delete.
- **Trust levels** — what you can read and write at each tier (0–3).
- **Tool dispatch** — which tool to call for which task.
The body below is the live content.
## Detect that you have MemClaw access
You have MemClaw if **any** of these are true in your runtime:
- A tool named `memclaw_recall`, `memclaw_write`, or `memclaw_doc` appears in your tool list.
- The host config (`.mcp.json`, `claude_desktop_config.json`, `~/.cursor/config.json`, or `~/.openclaw/openclaw.json`) contains an `mcpServers.memclaw` entry.
- A `MEMCLAW_API_KEY` or `MEMCLAW_API_URL` environment variable is set.
If yes: **call `memclaw_keystones` once at session start, before any other action**, then follow the rules below.
---
../../snapshots/skill.md
---
## Endpoints for agent runtimes
- Plain Markdown of this skill: [`/docs/agents.md`](/docs/agents.md)
- All docs concatenated as plain text: [`/llms-full.txt`](/llms-full.txt) (index only: [`/llms.txt`](/llms.txt))
- Live tool schemas (JSON): [`/api/v1/tool-descriptions`](/api/v1/tool-descriptions)
- Live OpenAPI spec (JSON): [`/api/openapi.json`](/api/openapi.json)
- One-line install on a runtime (env-aware — uses the host this page is served from):
Provenance: the SKILL content above is committed to [`caura-ai/caura-memclaw`](https://github.com/caura-ai/caura-memclaw) at `static/skills/memclaw/SKILL.md` and rebuilt on every release.
# List Document Collections
URL: https://memclaw.net/docs/api-reference/documents-collections
Description: GET /api/v1/documents/collections — list document collections in a tenant (added in PR
# Search Documents
URL: https://memclaw.net/docs/api-reference/documents-search
Description: POST /api/v1/documents/search — semantic + keyword search across stored documents (added in PR
# Health
URL: https://memclaw.net/docs/api-reference/health
Description: GET /api/v1/health — liveness probe.
# API Reference
URL: https://memclaw.net/docs/api-reference
Description: Auto-generated REST reference, sourced from the FastAPI OpenAPI spec.
This reference is generated from the live FastAPI spec served at `/api/openapi.json`. The committed snapshot at [`openapi.snapshot.json`](https://github.com/caura-ai/caura-memclaw/blob/main/frontend/site/openapi.snapshot.json) is the build-time source of truth; CI fails when it drifts.
Regenerate locally with:
```bash
API_BASE_URL=https://memclaw.net npm run regen:openapi # managed
API_BASE_URL=http://localhost:8000 npm run regen:openapi # self-hosted
```
## Authentication
All endpoints accept `X-API-Key`. For per-tenant keys (`mc_*`), also send `X-Tenant-ID`. See [Auth modes](/docs/reference/env-vars#auth-modes).
Errors follow the [canonical envelope](/docs/reference/errors) (`{ error: { code, message, details? } }`). REST responses also keep a top-level `detail` for back-compat.
## Curated pages
A hand-picked subset rendered with the [``](https://www.fumadocs.dev/docs/integrations/openapi/api-page) component. The list is short by design; for everything else, fetch the live OpenAPI directly.
- [Write a memory](/docs/api-reference/write) — `POST /api/v1/memories`
- [Recall](/docs/api-reference/recall) — `POST /api/v1/recall`
- [Stats](/docs/api-reference/stats) — `GET /api/v1/memories/stats`, `GET /api/v1/stats`
- [Keystones](/docs/api-reference/keystones) — `GET / POST / DELETE /api/v1/memclaw/keystones`
- [Search documents](/docs/api-reference/documents-search) — `POST /api/v1/documents/search`
- [List collections](/docs/api-reference/documents-collections) — `GET /api/v1/documents/collections`
- [Health](/docs/api-reference/health) — `GET /api/v1/health`
## The full surface
For the complete set (admin · agents · audit-log · crystallize · documents · entities · evolve · fleet · graph · ingest · insights · keystones · memories · plugin · recall · search · settings · stats · stm · tenants · whoami), use the live Swagger UI at [`/api/docs`](/api/docs) or pull the spec at [`/api/openapi.json`](/api/openapi.json).
# Keystones
URL: https://memclaw.net/docs/api-reference/keystones
Description: GET / POST / DELETE /api/v1/memclaw/keystones — mandatory governance rules.
Keystones are MANDATORY policy rules served to every agent on session start. See [Concepts → Keystones](/docs/concepts/keystones) for what they're for and how scope + weight + trust gating work.
## List
Open (no trust gate). The plugin fetches this on every session boot.
## Upsert
Trust ≥ 1 for `scope=agent` targeting the caller's own `agent_id`; trust ≥ 2 otherwise.
## Delete
Same dynamic trust gating as upsert. The platform fetches the stored row first to read its scope/agent_id before computing the required trust level.
# Recall
URL: https://memclaw.net/docs/api-reference/recall
Description: POST /api/v1/recall — hybrid vector + keyword + entity search.
# Stats
URL: https://memclaw.net/docs/api-reference/stats
Description: GET /api/v1/memories/stats — aggregate counts (PR
The `memclaw_stats` MCP tool added in PR #64 calls `compute_memory_stats()`, which is shared with the REST handler below. Both surfaces return `{total, by_type, by_agent, by_status, scope}` (REST omits `scope`).
## Memory stats (auth)
## Public stats
A lightweight unauthenticated counters endpoint used by the marketing-site status bar.
# Write a memory
URL: https://memclaw.net/docs/api-reference/write
Description: POST /api/v1/memories — persist content for later recall.
# The Broker Fleet Screen
URL: https://memclaw.net/docs/broker-fleet/dashboard
Description: A field guide to every column, status, panel, and action on the dashboard's Broker Fleet screen — installs, statuses, fleet policy, commands, the reported-agents panel, and revoke.
The **Broker Fleet** screen is where you manage the brokers joined to
your fleets: onboard machines, read their live status, push policy, act
on a specific install, and inspect the agents each one reports. This
page explains every part of it.
## The installs table
Each row is one **install** — a broker on one machine, identified by an
install UUID. The columns:
- **Tenant / Fleet** — which tenant owns it and which fleet (if any) it
joined.
- **Status** — a live health/liveness readout (below).
- **Last seen** — the timestamp of the most recent heartbeat (fleet-mode
installs only).
Use the status filter and fleet filter to narrow large fleets; the
counts at the top (**Online**, **Offline**) summarize the current page.
### What each Status means
Status is computed per row from the install's mode, revocation state,
and heartbeat age:
| Status | Meaning |
|---|---|
| **Online** (green) | Fleet-mode; last heartbeat within **3 minutes**. |
| **Stale** (yellow) | Fleet-mode; last heartbeat **3–15 minutes** ago. |
| **Offline** (grey) | Fleet-mode; last heartbeat **older than 15 minutes** (or none yet — "pending"). |
| **Active** (green) | **Personal-mode** install — registered and not revoked. Personal brokers don't heartbeat (liveness is fleet-only), so there's no online/offline to show. |
| **Revoked** (red) | The install's credential has been revoked; it can no longer talk to the cloud. |
The decision order is: **Revoked** wins; else if in a fleet, the
heartbeat-age bucket (**Online / Stale / Offline**); else **Active**.
**Active** is a *registration* state for a personal-mode install, not a
health signal. It only means "registered, not revoked." Liveness
(Online/Stale/Offline) exists only for fleet-mode brokers, which
heartbeat.
## Onboard a broker
The onboarding control mints a join key (single-use by default, or
reusable) and shows a **Copy install command** you paste on the target
machine. That whole flow —
including macOS/Linux/Windows specifics — is covered in
[Onboarding a Broker](/docs/broker-fleet/onboarding).
## Push fleet policy
Publishes the next **policy version** to a fleet. The server stamps the
version and effective time; connected brokers receive it live over
their policy stream (an offline broker picks it up on reconnect), with
no restart. The document is JSON — you own the `rules` array and
`categorical_settings`; the server owns the envelope. For example:
```json
{
"rules": [
{ "rule_id": "deny-secrets-writes",
"predicate": { "field": "tool", "op": "eq", "value": "write" },
"action": "deny", "severity": "high", "reason": "no secret writes" }
],
"categorical_settings": { "auto_wire_agents": true }
}
```
`categorical_settings.auto_wire_agents: true` opts the fleet into
zero-touch onboarding of newly detected-but-unconnected agents — each
broker wires them on its own on the next policy apply, without a
per-machine command.
## Issue command
Queues a **cloud command** for one install and delivers it live over the
policy stream (an offline broker replays it on reconnect; every command
is acknowledged). The dialog has a **Command type** dropdown and an
optional JSON **payload**:
| Command type | What it does |
|---|---|
| **`agents.rescan`** | Re-detect the machine's agents, record and issue identities for wired ones. With payload `{"wire": true}` it also **wires** any detected-but-unconnected agent (writes its config, with backups) — the way to onboard an agent installed after the broker. |
| `restart` / `rotate_credential` / `flush_queue` | Reserved command types. Current brokers acknowledge these as **`unsupported`** (they're defined in the protocol but not yet implemented broker-side), rather than failing silently. |
The dialog *queues* the command; the dashboard does not currently render
the acknowledgement or outcome. To confirm what happened, look on the
machine: `memclaw logs` shows the command received / executed / acked,
and the broker's audit log records each execution. A dashboard
command-history view is a planned follow-up.
## The Reported agents panel
For a selected install, this panel lists the agents the broker reported
on its **last heartbeat** — a snapshot as of *last seen*, not a live
view. Per agent:
- **Agent** — the agent type (claudecode, codex, cursor, gemini) and its
identity UUID.
- **Tier** — the wiring tier: `full` (hooks) or `strong` (MCP).
- **State** — **Active** (identity issued, not revoked) or **Tombstoned**
(revoked/uninstalled).
- **Wiring** — the integration verdict for an active agent as of that
heartbeat: **Wired** (detected with a clean config plan), **Not wired**
(detected but drifted, or its broker config was stripped), or **—**
(unknown — tombstoned, or the broker couldn't resolve the product).
- **First seen / Last reported** — when the identity first appeared and
the timestamp of the heartbeat that carried it.
**State: Active** means the agent's *identity* is issued and not
revoked — on its own it does **not** guarantee the agent is currently
wired and able to route memory. The **Wiring** column is that health
signal: if someone removes an agent's broker config it stays **Active**
but flips to **Not wired**. Both are a snapshot as of the last heartbeat,
not a live view — for the real-time verdict, `memclaw status` on the
machine remains the source of truth for whether an agent is actually
`integrated`.
## Revoke install
Revokes the install's credential: the cloud hard-rejects its subsequent
calls (403), and a targeted `install.revoked` event tells a connected
broker to fail closed. Use it when a machine is decommissioned or a
credential may be compromised. It also cancels the install's pending
commands. The row then shows **Revoked**.
# How Onboarding Works
URL: https://memclaw.net/docs/broker-fleet/how-it-works
Description: The credential model behind broker onboarding — the register-only join key vs the per-machine install credential, how fleets bind, single-use vs reusable keys, where credentials live, the write-ownership boundary, and the security properties. Read this to plan a fleet rollout.
[Onboarding a Broker](/docs/broker-fleet/onboarding) is the step-by-step how-to.
This page is the **how-it-works** — the model you need to plan a rollout across a
company: how many keys to mint, when to make one reusable, which fleet a key
enrolls into, where each broker's credential lives (for a security review), and
what actually happens if a join URL leaks.
## Two credentials, two jobs
The single most important thing to understand: a broker uses **two different
credentials at two different moments**. Conflating them is the source of most
confusion.
| | **Join key** | **Install credential** |
|---|---|---|
| Prefix | `mc_…` | `mci_v1_…` |
| Who holds it | you mint it; you paste/share it | the broker (one per machine) |
| When it's used | **once**, at register | **every** call after register |
| What it can do | **register only** | full data-plane (tenant-scoped) |
| Carries | the target tenant + (optional) fleet | the machine's identity |
| Where it lives | a join URL you copy | the broker's keychain (see below) |
- The **join key** is what rides in the install one-liner
(`MEMCLAW_JOIN_URL='…/join/mc_…'`). It is **register-only**: the cloud accepts
it *only* at `POST /installs/register` and rejects it (403) on every
data-plane endpoint (search, memories, MCP, dashboard). It carries the
destination — a tenant, and optionally a fleet.
- The **install credential** is minted by the cloud **at register**, unique to
that machine, and becomes the broker's steady-state credential. The daemon
sends it as the `X-API-Key` header on every subsequent call — claim,
heartbeat, the fleet-policy SSE stream, command acknowledgements, and audit-log
uploads.
The join key is a bootstrap artifact, not a data credential. Even if the join
URL leaks (a pasted chat message, browser history, a CI log), the worst case is
that someone enrolls a **rogue broker** into your fleet — which shows up in the
Broker Fleet dashboard, is revocable, and is bound to the enrolling machine's
fingerprint. There is **no path from a leaked join key to your memories**. This
is a deliberate least-privilege design (ADR-0001); it's what makes a *reusable*
key safe to drop into an MDM script or a shared channel.
## What actually happens during onboarding
### Mint (org-admin, in the dashboard)
**Broker Fleet → Onboard a broker** → pick the **home tenant** and **fleet**,
choose **Single-use** or not, and copy the install command (or the join URL).
The `mc_` key is created with those bindings baked in.
### Register (once, on the target machine)
`install.sh` downloads the binary and runs `memclaw setup --join`, which sends
the join key **one time** to `POST /installs/register`, along with the machine's
`install_uuid` and a machine fingerprint. The join key is never sent again.
### Exchange for a per-machine credential
The cloud validates the join key and returns an **install credential**
(`mci_v1_`), derived from *this machine's* `install_uuid`. The broker
stores it in its keychain. If the key was **single-use**, it is revoked now, in
the same transaction — a second machine presenting it gets a terminal 403.
### Join the fleet
The cloud binds the install to the join key's fleet and seeds a default policy if
that fleet is brand-new. The broker opens its policy SSE stream and receives the
current fleet policy live.
### Wire + run
Detected agents are wired to route through the broker; the daemon starts. From
here on, **every** cloud call authenticates with the install credential — the
join key is done.
## Fleets: one key → one fleet
A join key is **pinned to a single fleet** at mint time. The broker cannot
override it — there is no `--fleet` flag, and the register request carries no
fleet field; the fleet is resolved entirely from the key's server-side binding
and reported back to the broker.
- **To enroll brokers into several fleets, mint one join key per fleet.** The same
key can't be repointed at a different fleet.
- A key can also be **fleetless** (leave Fleet blank) — the broker registers to
the tenant without joining a fleet.
- Fleet names are **free-form labels** — there is no fleet registry to create
first. A name that's never been used is materialized (with a default policy) on
the first register that references it.
Because the fleet is a free-text label with no validation, a **typo at mint time
mints into a brand-new, one-broker fleet** rather than erroring (unlike the
tenant, which is validated against your org). When minting, copy the exact name
of an existing fleet — or deliberately choose a new one. Double-check the Fleet
field before you hand the key out.
## Single-use vs reusable — planning a rollout
The **Single-use** toggle (on by default) is the one decision that shapes a
rollout:
- **Single-use** — the key self-revokes after the first successful register. Use
it for a one-off host or a high-sensitivity onboarding where the key should
work exactly once.
- **Reusable** (untick Single-use) — the same key/URL enrolls **any number** of
machines into its fleet. Use it for a fleet rollout.
| Situation | Mint a key that is… |
|---|---|
| Rolling out to a team / fleet (MDM or IT push) | **reusable**, one per fleet |
| A single machine, or a sensitive host | **single-use** |
| Automated / repeated onboarding (CI, nightly e2e) | **reusable** |
Reusability affects **only** the bootstrap step. Every machine still receives its
**own** install credential (next section) — a reusable key is not a shared
steady-state secret. Revoke a key any time from **API Credentials**, or revoke an
already-enrolled broker from **Broker Fleet → Brokers**.
## Where the credential lives (for security review)
Each broker's install credential is stored **only on that machine**, in its
keychain — never in an agent's config file, and never shown again after register.
- **Default:** the OS keychain — macOS Keychain, Linux Secret Service, or Windows
Credential Manager.
- **Headless hosts** (a CI runner, a VM over SSH, a container with no desktop
session): set `MEMCLAW_KEYCHAIN=file` and the credential is written to a
`0600` JSON file at `/identity/keychain.json`. `install.sh`
selects this automatically when it detects no session keychain.
- **Cloud side** stores only a **SHA-256 hash** of the credential — the raw token
never leaves the machine.
- **Per-broker, not shared.** The credential is HMAC-derived from the machine's
own `install_uuid`, so two brokers onboarded with the same reusable key still
hold **distinct** credentials. Revoking one broker never affects another.
## Which agent a broker's writes belong to (the ownership boundary)
A broker writes memories on behalf of the agents it fronts, and each write names
the **agent id** it should be attributed to. Because one install can name *any*
agent id, the cloud enforces an **ownership boundary**: an install may only
attribute a memory to an agent it **owns**. This keeps two brokers in the same
tenant — for example, two machines onboarded from the same reusable key — from
writing under each other's agent identities.
- **First touch owns.** The first install to write as a given agent id becomes its
owner — the cloud stamps that install's identity onto the agent. No setup; it
happens on the first write.
- **A foreign name is redirected, never blocked.** If a *different* install later
names that agent, its write is transparently re-attributed to that install's own
fallback identity, `broker:`, instead of the owned agent. The write
still succeeds — it just lands under the caller's own identity, never the owner's.
- **Same-install writes pass through unchanged.** An install writing as its own
agent always keeps that name.
The boundary holds on **every** write surface a broker can reach — single and bulk
memory writes, document ingest, and short-term-memory promotion — so no endpoint
bypasses it.
An install's fallback identity, `broker:`, is reserved: an install
can only ever write as its *own* fallback. This stops a broker from pre-claiming
another install's (guessable) fallback id in order to capture that install's
redirected writes.
This is defense-in-depth. A normal broker only ever writes as its own agents, so the
redirect is a backstop rather than a path anyone hits in day-to-day operation — it's
there so a compromised or misconfigured broker cannot forge attribution onto an agent
another install owns.
## Prerequisites & who can do what
- **A tenant must exist first.** The join key is minted against a home tenant that
already belongs to your org (the mint form only lists your org's tenants).
- **A fleet does not need to exist first** — see above.
- **Minting** a join key is an **org-admin** action. **Running** the installer on
the target machine needs only a shell and the copied command — no MemClaw
account on that machine.
## FAQ
**Do I need a tenant and a fleet before onboarding?**
A tenant, yes (it's the key's home tenant). A fleet, no — name any fleet at mint
and it's created on first register.
**Can I onboard many brokers with one command?**
Yes — mint the key **reusable** (Single-use off). Single-use keys work for exactly
one machine.
**Can one key enroll brokers into different fleets?**
No. A key is pinned to one fleet. Mint a separate key per fleet.
**After onboarding, what does a broker authenticate with?**
Its own per-machine **install credential** (`mci_v1_…`) in the `X-API-Key` header
— not the join key. The join key is used only once, at register.
**Do all my brokers share a key?**
No. Each machine gets a unique install credential, even when they were onboarded
with the same reusable join key.
**Can one broker write memories as another broker's agent?**
No. The first install to write as an agent id owns it; a different install naming
that agent has its write redirected to its own `broker:` identity —
it cannot attribute a memory to an agent another install owns. This holds on every
write surface (memories, bulk, ingest, STM promote). See [the ownership
boundary](#which-agent-a-brokers-writes-belong-to-the-ownership-boundary).
**Where is the credential kept, and can I audit it?**
In the broker's keychain (OS keychain, or a `0600` file under `MEMCLAW_HOME` with
`MEMCLAW_KEYCHAIN=file`). The cloud keeps only a hash; the raw token stays on the
machine.
**A join URL leaked — what's the exposure?**
It's register-only, so the worst case is a rogue broker enrolling into the fleet
— visible in the dashboard, fingerprint-bound, and revocable. No access to
memories. Revoke the key in API Credentials and revoke any unexpected broker in
Broker Fleet → Brokers.
**How do I remove a broker?**
Revoke it from **Broker Fleet → Brokers** (hard-revokes its credential and closes
its fleet membership), or run `memclaw leave-fleet` on the host to unbind it.
# Broker Fleet
URL: https://memclaw.net/docs/broker-fleet
Description: What the MemClaw broker is, how personal and fleet mode differ, and how a fleet of brokers is governed from the dashboard.
The **broker** (`memclaw`) is a small local daemon that runs on a
developer's machine and connects their coding agents — Claude Code,
Codex, Cursor, Gemini — to MemClaw. It is the enforcement point: memory
reads and writes pass through it, so policy, redaction, and a
tamper-evident audit log apply *before* anything leaves the machine.
A **broker fleet** is a set of those brokers joined to one fleet and
governed together from the dashboard's **Broker Fleet** screen — one
place to onboard machines, push a policy to all of them, see which are
online, and act on a specific install.
The **broker fleet** (this section) is about the *machines and their
daemons*. The **[memory fleet](/docs/tutorials/multi-agent-fleet)** is
about *shared memory* — many agents drawing on one governed brain. They
compose: a broker joins a memory fleet so its agents share common
ground, and the broker fleet screen is how you manage the brokers that
do it.
## Personal mode vs fleet mode
A broker runs in one of two modes, and the mode decides what the
dashboard can see and do:
| | Personal mode | Fleet mode |
|---|---|---|
| Joined to a fleet | No | Yes |
| Heartbeats to the cloud | No (privacy: liveness is fleet-only) | Yes, every 60s |
| Governed by fleet policy | No (local policy only) | Yes (fleet policy merges over local) |
| Shows in Broker Fleet screen | As **Active** (registered, no liveness) | As **Online / Stale / Offline** by heartbeat age |
| Receives cloud commands | No | Yes (over the policy stream) |
Onboarding a machine with a **join URL** (below) puts it in fleet mode.
A broker registered without a fleet stays personal — fully functional
locally, just not centrally managed.
## What onboarding actually does
When you run the one-command installer from the dashboard, the broker:
1. **Registers** with the cloud using the bootstrap key baked into the
join URL (single-use by default, or reusable if minted that way),
receiving its install credential (bound to your tenant).
2. **Joins the fleet** the key was minted for.
3. **Wires the detected agents** — writes the hook / MCP config each
agent needs to route through the broker, backing up anything it
replaces so `memclaw uninstall` can revert cleanly.
4. **Starts the daemon** and writes a boot service so it comes back on
login.
5. **Heartbeats** — the first beat carries the machine's agent
inventory, which is what fills the **Reported agents** panel.
The next pages walk through [onboarding a new broker](/docs/broker-fleet/onboarding)
step by step, then [how onboarding works](/docs/broker-fleet/how-it-works) — the
credential model, fleets, and security — and finally
[every detail on the Broker Fleet screen](/docs/broker-fleet/dashboard).
# Onboarding a Broker
URL: https://memclaw.net/docs/broker-fleet/onboarding
Description: Install and join a new broker to your fleet with one command — what to run on macOS, Linux, and Windows, what each step does, and how to verify it worked.
Onboarding a machine takes one copy-paste on macOS and Linux. This page
walks through it and covers Windows (which needs a different path) and
verification. For the credential model, fleet binding, single-use vs
reusable keys, and where each broker's credential lives, see
[How onboarding works](/docs/broker-fleet/how-it-works).
Minting a join key is an **org-admin** action in the dashboard. The
person running the installer on the target machine only needs a shell
and the copied command — no MemClaw account.
## The happy path (macOS & Linux)
### Mint the join command
In the dashboard, go to **Broker Fleet → Onboard a broker**, pick the home
tenant and fleet, and click **Copy install command**. You get a
one-liner like:
```sh
curl -sSf https://memclaw.net/install.sh | MEMCLAW_JOIN_URL='https://memclaw.net/join/mc_…' sh
```
The host in the command is the environment you copied it from — copy
from staging, it targets staging; copy from prod, it targets prod. The
`mc_…` token is a bootstrap key scoped to your tenant and the chosen
fleet. By default it's **single-use** (consumed on first register); untick
**Single-use** in the mint form to get a **reusable** key for rolling out
many machines or repeated / automated onboarding.
### Run it on the target machine
Paste it into a shell on the machine you're onboarding. You do **not**
pick an OS or architecture — the script detects them and pulls the
right build:
- **macOS** → a universal binary (Intel and Apple Silicon, one download).
- **Linux** → the matching `x86_64` or `arm64` build.
The same copied command works on any of them.
### Let it register, join, wire, and start
The installer runs end to end without prompts:
1. **Register** with the cloud using the bootstrap key → receives the
install credential (bound to your tenant).
2. **Join the fleet** the key was minted for.
3. **Wire the detected agents** — writes each agent's hook / MCP config
to route through the broker, backing up anything it replaces.
4. **Start the daemon** and write a boot service (loads on login).
It finishes with `✓ memclaw is set up (cloud-connected).`
### Restart your coding agents
The agents load their integration config at launch, so **restart Claude
Code / Cursor / etc.** once for the wiring to take effect. Already-open
sessions won't route through the broker until they're restarted.
## Windows
The one-command installer does **not** run on native Windows — `curl |
sh` needs a POSIX shell, which PowerShell/cmd don't provide. Use one of:
- **WSL (recommended)** — run the exact copied command inside a WSL
shell. It installs the Linux build and behaves identically to a Linux
host.
- **Manual** — download `memclaw__windows_.zip` from the
release, extract it, and run `memclaw setup --join ''`
yourself (paste the join URL from the same mint dialog).
You may see references to a Homebrew or Scoop tap, or `npx`/`uvx`
wrappers. Those distribution channels are not published yet — WSL and
the manual `.zip` are the supported Windows paths today.
## Verifying it worked
**On the machine:**
```sh
memclaw status
```
Look for `daemon: running`, `mode: fleet`, `cloud … reachable: true`,
and each agent listed as `integrated`. `memclaw status --json` includes
the daemon's `log_path`; `memclaw logs` tails it.
**In the dashboard:** the install appears under **Broker Fleet** as
**Online**, and within a minute its **Reported agents** panel lists the
wired agents (claudecode, codex, cursor, gemini) with their tiers. See
the [Broker Fleet screen reference](/docs/broker-fleet/dashboard) for
what every column and panel means.
## Troubleshooting
On a headless Linux box (no desktop session / Secret Service — e.g. a
CI runner or VM over SSH) the OS keychain can't unlock and the install
fails at credential storage. Re-run with the file-backed keychain:
```sh
curl -sSf https://memclaw.net/install.sh | MEMCLAW_KEYCHAIN=file MEMCLAW_JOIN_URL='…' sh
```
The installer sets this automatically when it detects no session bus,
but pass it explicitly if you hit the error.
- **`no changes to apply`** on a re-run is normal — the broker is
idempotent; it re-mints only what's missing.
- **An agent installed *after* onboarding** isn't wired automatically by
a re-run of a different agent. Re-run `memclaw install` on the machine
(it wires and issues the new one immediately), or have an operator
issue an `agents.rescan` command with `{"wire": true}` from the
dashboard — see the [screen reference](/docs/broker-fleet/dashboard#issue-command).
# Architecture
URL: https://memclaw.net/docs/concepts/architecture
Description: The services that make up MemClaw, what each one is responsible for, and how a write flows from client through core-api, the event bus, the worker, and storage.
Three services, one event bus, Postgres + pgvector underneath. Synchronous on the response path, async on the heavy work.
## core-api
FastAPI service. Handles HTTP and MCP requests — the MCP server is mounted at `/mcp` on the same app, so a single deployment serves both surfaces. It applies auth and quotas, writes raw memories, and serves recall reads. After a successful write, core-api publishes `embed-requested` / `enrich-requested` events to the event bus for the worker to pick up. Contradiction detection runs **post-commit, fire-and-forget** via an async `track_task` (see `services/contradiction_detector.py`'s `detect_contradictions_async`) — the write itself does not block on it.
## core-worker
Async worker. Subscribes to `embed-requested` and `enrich-requested` events on the event bus (in-process for OSS single-process; Pub/Sub on the enterprise platform). Calls the configured embedding + entity-extraction providers, then writes the structured rows back through `core-storage-api`.
## core-storage-api
Thin Postgres gateway used by both `core-api` and `core-worker` for memory CRUD and pgvector ops. Two-worker default; bulk operations cap at ~60 req/s — bump to 8 workers via an override before backfills.
## Event bus
OSS default is an in-process bus; the enterprise / managed deployment uses Google Pub/Sub. The `/api/v1/health` route's `event_bus` field reports the current backend's status.
## OpenClaw plugin (optional)
Lives inside an OpenClaw gateway and claims the `memory` slot, replacing `memory-core` and exposing the `memclaw_*` tools to every agent that runs through the gateway. See [OpenClaw integration](/docs/integrations/openclaw).
# Cross-tenant credentials
URL: https://memclaw.net/docs/concepts/cross-tenant-credentials
Description: Read across every tenant in your org with a single credential — for admin agents, analytics, and rollups.
A **cross-tenant credential** lets one agent read memories, documents, and stats across multiple tenants in an org without rotating keys when tenants are added or removed. Writes still pin to the credential's home tenant — the widening is read-only by construction.
The headline use case: a single Admin agent that summarises activity across every fleet, surfaces conflicts that span teams, or builds an org-wide knowledge view. Mint the credential once; it stays current as your org grows.
## What the kind buys you
| Capability | Tenant-scoped (`user_api_key`, `agent_key`) | Cross-tenant (`cross_tenant`) |
| --- | --- | --- |
| Read from home tenant | ✅ | ✅ |
| Read from sibling tenants in the org | ❌ (403) | ✅ |
| Write to home tenant | ✅ | ✅ |
| Write to sibling tenants | n/a | ❌ (403 — writes pin to home) |
| Auto-include tenants added after mint | n/a | ✅ (with `read_all_org_tenants=true`) |
| Wire prefix | `mc_` | `mc_` (kind is on the row, not the prefix) |
## Two ways to scope the read set
Pick one at mint time:
### `source_tenant_ids` — a frozen, explicit list
Pin the readable set to specific sibling tenants. The list is recorded on the credential row; adding a new tenant to the org does **not** widen this credential. Rotate the credential to include new tenants.
Use when the cross-tenant relationship is bounded by intent (e.g. "the EU rollup agent reads from `eu-sales`, `eu-support`, `eu-product` and never anywhere else"). A future tenant added to the org should *not* be readable by this credential without an explicit decision.
### `read_all_org_tenants=true` — live resolution
Re-resolves the readable set on every authenticated request by querying the current `enterprise.tenants` membership for the credential's org. New tenants are auto-included on the next request — no rotation needed. Removed (soft-deleted) tenants disappear from the readable set on the same cadence.
Use when "every tenant in the org" *is* the policy — the org Admin agent, the compliance reporter, the cross-team weekly summary.
The two modes are mutually exclusive — a single credential carries either an explicit `source_tenant_ids` list or `read_all_org_tenants=true`, never both. The admin-api enforces this at mint time.
## Mint via the dashboard
Settings → Organization → API Credentials → **Add credential** → **Cross-tenant**. The wizard presents a binary "Read scope" toggle:
- **Home tenant only** — produces a regular `user_api_key`/`agent_key` row.
- **Read across all org tenants** — produces a `cross_tenant` row with `read_all_org_tenants=true`. The static-list variant (`source_tenant_ids`) is reserved for the API; the dashboard intentionally surfaces only the live-resolution mode because the audit-evidence trail concluded most operators mean "all, current and future" when they ask for cross-tenant.
The `raw_key` is returned **once**. Treat it like a password.
## Mint via the API
```bash
curl -X POST "https://memclaw.net/api/v1/admin/orgs/$ORG_ID/api-credentials" \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"kind": "cross_tenant",
"home_tenant_id": "admin-fleet",
"read_all_org_tenants": true,
"label": "org-admin-summary-agent",
"capabilities": ["read", "write"]
}'
```
For the static-list variant, swap `read_all_org_tenants: true` for `source_tenant_ids: ["sibling-a", "sibling-b"]`.
## What you'll see on the wire
The gateway plumbs the resolved scope on every request:
```
X-Tenant-ID: admin-fleet
X-Readable-Tenant-IDs: admin-fleet,sibling-a,sibling-b,sibling-c
X-Capabilities: read,write
X-Auth-Mode: cross-tenant
```
`/whoami` surfaces the same shape so you can verify your credential without probing each tenant:
```json
{
"tenant_id": "admin-fleet",
"readable_tenant_ids": ["admin-fleet", "sibling-a", "sibling-b", "sibling-c"],
"capabilities": ["read", "write"],
"auth_mode": "cross-tenant",
"via_gateway": true
}
```
## Which surfaces honor the widening
Cross-tenant credentials widen `WHERE tenant_id = $home` to `WHERE tenant_id = ANY($readable)` on these read paths:
- **MCP tools:** `memclaw_recall`, `memclaw_list` (scope=`fleet`|`all`), `memclaw_stats` (scope=`fleet`|`all`), `memclaw_doc` ops `search`/`read`/`query`/`list_collections`.
- **REST search/recall:** `POST /search`, `POST /recall`, `POST /documents/search`.
- **REST aggregate lists:** `GET /memories` (when no explicit `tenant_id` query param), `GET /memories/stats`, `GET /documents/collections`.
For the storage-routed reads (`GET /documents/{id}`, `POST /documents/query`, `GET /documents`, `GET /memclaw/keystones`), cross-tenant credentials access **one tenant at a time** — pin `tenant_id` to any tenant in your readable set and the gate allows it.
Writes always pin to `home_tenant`:
- ❌ `memclaw_write` with `tenant_id` not equal to home → `FORBIDDEN`.
- ❌ `POST /documents` (write) to a sibling tenant → 403.
- ❌ `POST /memories` to a sibling → 403.
The principle: the credential carries `{read, write}` capabilities, but `enforce_tenant` on the write target rejects anything that isn't home. Reads use `enforce_readable_tenant`, which accepts any tenant in the readable set.
## Audit trail
Every widened read emits a `cross_tenant_read` audit event **per source tenant** the query touched. Each event is logged TO the source tenant (so per-tenant audit-log queries surface "who read FROM my tenant") with the home tenant_id and agent_id in `detail` for forensic traceability:
```json
{
"action": "cross_tenant_read",
"tenant_id": "sibling-a",
"agent_id": "admin-rollup-bot",
"resource_type": "memclaw_recall",
"detail": {
"home_tenant_id": "admin-fleet",
"home_agent_id": "admin-rollup-bot",
"result_count_from_this_tenant": 7,
"query_summary": "what did we ship last week"
}
}
```
Single-tenant credentials emit zero `cross_tenant_read` events — the audit is gated on the readable set being wider than home.
## Operational notes
- **Live resolution on every request.** The `read_all_org_tenants=true` path queries `list_tenants_by_org` on every authenticated call. For high-traffic admin agents this is the next caching target — a short in-process LRU keyed on `org_id` would amortise the round-trip without making new tenants invisible for longer than feels noticeable.
- **Fail-open on storage degradation.** If the live-resolution lookup fails, the credential degrades to home-only rather than 5xx-ing the whole request. Logged for observability; the credential keeps working with reduced scope until storage recovers.
- **Revocation is immediate.** Setting `revoked_at` short-circuits the resolver before the org-tenants lookup — revoked credentials can't even probe org membership.
- **The on-the-wire `mc_` prefix is shared.** Tenant-scoped and cross-tenant credentials look identical at the protocol layer. The dashboard's "Kind" column tells you which is which.
## When NOT to use cross-tenant
- **Per-fleet bots** that should only see their own tenant's memories — use a regular `agent_key`. The trust-and-fleet system handles intra-tenant scoping; cross-tenant widening is for org-wide views.
- **Customer-facing integrations** where a single org's tenants represent different customer accounts — use one tenant-scoped credential per customer, not one cross-tenant credential that erases the boundary.
- **Write-heavy agents** — cross-tenant only widens *reads*. If your agent's primary job is writing across many tenants, run one tenant-scoped agent per tenant instead and use a coordinator pattern at the application layer.
## Related
- [Trust levels](/docs/concepts/trust-levels) — the per-agent permission scheme that gates reads/writes within a tenant.
- [Governance](/docs/concepts/governance) — org/tenant/agent hierarchy and how cross-tenant credentials fit into it.
- [Per-agent keys](/docs/integrations/per-agent-keys) — agent-scoped credentials, the more common scope for production fleets.
# Governance
URL: https://memclaw.net/docs/concepts/governance
Description: Keystones, trust enforcement, the Karpathy Loop, and the Memory Crystallizer.
MemClaw's governance is four mechanisms working together. Each one operates at a different point in the agent's lifecycle.
## Keystones
**Mandatory policy rules** the platform serves to every agent on session start (`memclaw_keystones`). Scope-merged (`tenant` / `fleet` / `agent`), weight-ordered, and *non-negotiable* — they override conflicting user instructions. See the dedicated [Keystones page](/docs/concepts/keystones) for the model and authoring flow.
Author with `memclaw_keystones_set` (trust ≥ 1 for self, ≥ 2 for cross-agent / fleet / tenant). REST: `GET / POST / DELETE /api/v1/memclaw/keystones`.
## Trust enforcement
Every API call checks the calling agent's trust level (see [Trust levels](/docs/concepts/trust-levels)). The check happens server-side in `core_api.services.trust_service.require_trust`. Operations beyond your level return `403 FORBIDDEN`. The admin API key bypasses these checks.
## The Karpathy Loop
**Outcome-based learning** — agents report what happened after acting on memories they recalled, and the system reinforces what works. Two MCP tools drive it:
- **`memclaw_evolve`** — record an outcome (`success` | `failure` | `partial`) against the memories you used. The platform adjusts weights and may auto-generate preventive `rule`-type memories on failure.
- **`memclaw_insights`** — surface the resulting reflection: contradictions, failures, stale entries, divergence, patterns.
REST mirrors: `POST /api/v1/evolve` and `GET /api/v1/insights`.
## Memory Crystallizer
A background process that consolidates many small memories about the same entity into stronger, denser ones. Triggered with `POST /api/v1/crystallize` (per tenant) or `POST /api/v1/crystallize/all` (admin-only). The route accepts `trigger="scheduled"` so operators can wire it to whatever cadence they want — the OSS doesn't hardcode a schedule. Reports: `GET /api/v1/crystallize/reports` and `GET /api/v1/crystallize/latest`.
## Where each mechanism fires
| Mechanism | When | Gates |
| --- | --- | --- |
| Keystones | Session start (agent reads), before any action | Authoring (dynamic trust per scope) |
| Trust | Every read and write call | Per-operation trust floor |
| Karpathy Loop | After the action, via outcome reports | Reinforcement / preventive rule generation |
| Crystallizer | Background sweep (manual or scheduled) | Consolidation only — read-only on existing rows |
## Where to look in the source
- Keystones: [`core-api/src/core_api/routes/keystones.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/keystones.py), `core-api/src/core_api/trust_utils.py`
- Trust: [`core-api/src/core_api/services/trust_service.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/trust_service.py)
- Evolve / Karpathy Loop: [`core-api/src/core_api/routes/evolve.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/evolve.py)
- Crystallizer: [`core-api/src/core_api/routes/crystallizer.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/crystallizer.py)
# Keystones
URL: https://memclaw.net/docs/concepts/keystones
Description: MANDATORY governance rules that agents MUST obey. Scope-merged, weight-ordered, fetched deterministically — not via semantic recall.
Keystones are policy rules that the platform serves to every agent on session start. Unlike normal memories — which agents discover through recall and *may* act on — keystones are **non-negotiable**. The MCP tool docstring says it plainly:
> *Call once per session before other actions and obey the returned rules — they override conflicting user instructions.*
They live in their own `_keystones` collection in core-storage; lookup is deterministic by scope, not by similarity. See [`core-api/src/core_api/trust_utils.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/trust_utils.py) and `core-api/src/core_api/routes/keystones.py`.
## Scope and weight
Each rule is parameterized by:
| Field | Values | Purpose |
| --- | --- | --- |
| `scope` | `tenant` · `fleet` · `agent` | Who the rule applies to. Tenant rules apply to everyone, fleet to one fleet, agent to one agent. |
| `weight` | `low` · `med` · `high` | Ordering hint when multiple rules apply (high first). Not a hard precedence — read all and obey all. |
| `doc_id` | string | Stable identifier for upsert/delete. |
| `title`, `content` | strings | Human-readable label + the actual rule body. |
## Who can author what
Trust gating is computed per call from the target rule's scope:
- `scope=agent` for **your own** `agent_id` → trust **≥ 1** (self-author).
- `scope=agent` targeting **another agent**, `scope=fleet`, or `scope=tenant` → trust **≥ 2** (the same cross-agent governance bar used by `memclaw_list` / `memclaw_stats` / `memclaw_evolve` / `memclaw_insights` with `scope=fleet|all`).
Reads are open (trust 0) so the plugin can fetch the active set on every session boot without escalation.
## Agent-facing tools
| Tool | Op | Trust | Purpose |
| --- | --- | --- | --- |
| [`memclaw_keystones`](/docs/agents) | (read-only) | ≥ 0 | List scope-merged keystone rules. Call once per session. |
| `memclaw_keystones_set` | `set` | ≥ 1 (self) / ≥ 2 (other) | Upsert a keystone by `doc_id`. |
| `memclaw_keystones_set` | `delete` | ≥ 1 (self) / ≥ 2 (other) | Remove a keystone by `doc_id`. |
The canonical agent prompt covering these is rendered at [`/docs/agents`](/docs/agents) from upstream `SKILL.md`.
## Response shape
`memclaw_keystones` and `GET /api/v1/memclaw/keystones` both return:
```json
{
"count": 3,
"truncated": false,
"rules": [
{ "doc_id": "...", "title": "...", "content": "...", "scope": "tenant", "weight": "high", ... }
]
}
```
Note the merged set lives under `rules`, not `keystones` — the field name is fixed for backwards compatibility. `truncated: true` indicates the response was capped server-side; rules are ordered by weight (high → low), so the highest-priority rules are always delivered first.
## A gotcha worth knowing: `agent_id` semantics
On `memclaw_keystones_set` (and the REST equivalent), the `agent_id` field is the **TARGET** agent the rule binds to — NOT the caller. Caller identity comes from the API key. For `scope=tenant` and `scope=fleet` you must **omit** `agent_id` entirely; passing it returns `INVALID_ARGUMENTS`. Only `scope=agent` requires it.
## REST surface
Three endpoints under the `/api/v1/memclaw/keystones` prefix:
- `GET /api/v1/memclaw/keystones` — list (open).
- `POST /api/v1/memclaw/keystones` — upsert (dynamic trust).
- `DELETE /api/v1/memclaw/keystones/{doc_id}` — delete (dynamic trust).
See the [keystones API reference](/docs/api-reference/keystones) for the rendered schemas.
## How keystones relate to other governance
Keystones are **complementary** to the rest of MemClaw's governance, not a replacement:
- **[Trust enforcement](/docs/concepts/trust-levels)** gates *who* can do what. Keystones gate *what* anyone with sufficient trust must obey at runtime.
- **[Karpathy Loop](/docs/concepts/governance#the-karpathy-loop)** reinforces what works *after* the fact via outcome reports. Keystones operate *before* the action — they're hard rules the agent reads up front.
- **[Memory Crystallizer](/docs/concepts/governance#memory-crystallizer)** consolidates routine memories. Keystones are intentionally *not* memories — they're policies.
Use keystones for things like:
- *"Never store API keys in memory content"*
- *"Always confirm with the user before scope='all' deletes"*
- *"This fleet's data residency is EU only"*
# Memory Pipeline
URL: https://memclaw.net/docs/concepts/memory-pipeline
Description: What happens between calling memclaw_write and the memory becoming recallable.
Every write moves through four stages. Knowing them helps you debug "why didn't my agent recall this?" and "why does the stored memory look different from what I sent?".
## 1. Ingest
`POST /api/v1/memories` (or the `memclaw_write` MCP tool) validates auth, enforces the caller's [trust level](/docs/concepts/trust-levels) and tenant quota, and persists the row. The HTTP response returns as soon as the row is committed; everything else is asynchronous.
## 2. Enrich (async)
After the write returns, core-api publishes `embed-requested` and `enrich-requested` events to the [event bus](/docs/concepts/architecture#event-bus). `core-worker` consumes them and runs:
- **Embedding** — vector generation via the configured **embedding provider**. Default is OpenAI; alternatives include `local` and `fake` for development. Set with `EMBEDDING_PROVIDER` and `OPENAI_API_KEY` (or the matching keys for other providers).
- **Entity extraction + classification** — names, projects, repos, references; memory `type`, `title`, `summary`, `tags`. Driven by the configured **entity-extraction provider** (`ENTITY_EXTRACTION_PROVIDER`: `openai` / `anthropic` / `gemini` / `openrouter`). Vertex AI is the operator-managed platform-tier provider, used on the managed deployment.
Until enrichment lands, the memory is queryable by id but won't show up in semantic recall. The exact provider matrix and supported combinations live in the OSS [`.env.example`](https://github.com/caura-ai/caura-memclaw/blob/main/.env.example) header.
**Contradiction detection** also runs after commit (fire-and-forget via `track_task` — see `core-api/src/core_api/services/contradiction_detector.py`'s `detect_contradictions_async`). The write itself does not block on it; contradictions surface through `memclaw_insights` after the loop runs.
## 3. Govern
Independent of the write path, three governance mechanisms keep the store useful:
- **Trust enforcement** — every read and write checks the caller's `trust_level`. Operations beyond that level return `403 FORBIDDEN`. See [Trust levels](/docs/concepts/trust-levels) for the table.
- **Karpathy Loop** — agents report outcomes after acting on recalled memories via `memclaw_evolve` (`success | failure | partial`). The platform reinforces what works and may auto-generate preventive `rule`-type memories on failure. `memclaw_insights` surfaces the resulting reflection (contradictions, drift, stale entries).
- **Memory Crystallizer** — a separate background process that consolidates many small memories about the same entity into stronger denser ones. Triggered with `POST /api/v1/crystallize` per tenant, or `crystallize/all` (admin) on a nightly schedule.
## 4. Recall
`POST /api/v1/recall` (or `memclaw_recall`) does **hybrid retrieval**: vector similarity + keyword full-text + entity-graph matches, blended into a single ranked list. Trust still applies at read — a level-1 agent cannot recall across fleets it doesn't own.
For non-semantic flows:
- **`memclaw_list`** — paginated browse by entity / type / time. Use when `recall` is too narrow.
- **`memclaw_manage op=read`** — read by id when you already know the `memory_id`.
## Where each stage lives
| Stage | Service | Path in the OSS repo |
| --- | --- | --- |
| Ingest + recall + sync contradiction check | core-api | `core-api/src/core_api/routes/`, `services/contradiction_detector.py` |
| Embedding + entity extraction (async) | core-worker | `core-worker/src/core_worker/` |
| Storage (Postgres + pgvector) | core-storage-api | `core-storage-api/` |
For the live OpenAPI surface, see the [API Reference](/docs/api-reference).
# Trust Levels
URL: https://memclaw.net/docs/concepts/trust-levels
Description: The 4-tier agent permission scheme — what each level can read and write.
Source of truth: [`static/docs/integration-guide.md`](https://github.com/caura-ai/caura-memclaw/blob/main/static/docs/integration-guide.md#trust-levels) in the OSS repo. The numeric values shown here are the canonical values stored in the `agents.trust_level` column (`SmallInteger`, default `1`).
## The 4 tiers
| Level | Name | Permissions |
| --- | --- | --- |
| 0 | `restricted` | No read or write access. Use to temporarily disable an agent. |
| 1 | `standard` | Read and write within own fleet only. **Default for new agents.** |
| 2 | `cross_fleet` | Read across all fleets in the tenant; write within own fleet only. |
| 3 | `admin` | Read and write across all fleets; can delete memories. |
## Agent registration
Two paths put an agent in the system:
### Atomic provisioning (recommended for production)
`POST /api/v1/admin/agent-keys/provision` mints an **agent-scoped credential** **and** creates the Agent row in a single round-trip. You set `initial_trust` and `initial_fleet` in the request body and skip the lazy-create flow entirely. The response carries `agent_row_created: true` plus the raw `mc_` key (returned exactly once — save it). The credential's `mc_` prefix is shared with tenant-scoped keys; scope is bound at mint time on the credential row. Confirm with `GET /api/v1/whoami` using the new credential.
See [Per-agent keys](/docs/integrations/per-agent-keys) for the end-to-end curl + Python flow.
### Lazy auto-registration (legacy / OSS-only fallback)
Agents that connect with a tenant `mc_` key (or a self-hosted standalone deployment) are auto-registered on their first `memclaw_write`. The `fleet_id` from that first write becomes the agent's home fleet.
The starting trust level depends on a tenant setting (`agents.require_agent_approval` in `tenant_settings`):
- **Default — `require_agent_approval = false`:** new agent starts at trust **1** (`standard`) and can read + write in its home fleet immediately.
- **`require_agent_approval = true`:** new agent starts at trust **0** (`restricted`) — recall and single writes fail with `403` until an operator promotes the agent via the dashboard or `PATCH /api/agents/{agent_id}/trust`. (Bulk broker fan-in writes — `POST /api/v1/memories/bulk` — are deliberately **exempt** from this gate, so a broker onboarding a whole fleet isn't blocked on one not-yet-approved agent.)
Source: [`core-api/src/core_api/services/agent_service.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/agent_service.py) (`get_or_create_agent`) plus `DEFAULT_TRUST_LEVEL` in `core-api/src/core_api/constants.py`.
## Enforcement
Trust is checked on every API call. A level-1 agent attempting a cross-fleet recall gets `403`. The admin API key (`ADMIN_API_KEY`) bypasses trust enforcement entirely.
## Changing trust levels
From the dashboard (admin role required) or via the API:
Via API:
```bash
curl -X PATCH "$API/api/agents/{agent_id}/trust?tenant_id=$TENANT" \
-H "X-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"trust_level": 2}'
```
See the [API reference](/docs/api-reference) for the full set of agent endpoints.
# Delete your organization
URL: https://memclaw.net/docs/getting-started/delete-organization
Description: How to permanently delete a MemClaw organization, what gets destroyed, and when (and how) you can still recover.
You can delete your organization yourself, from the dashboard, at any time. The deletion is **immediate, atomic, and irreversible** — there is no grace window for customer-initiated deletes.
## Who can delete
Only an **owner** or **admin** of the organization can initiate self-delete. Members get a 403. If your account doesn't have the role to delete and there's nobody on the team who does, [contact support](mailto:support@caura.ai).
## How to delete
1. Sign in to [memclaw.net](https://memclaw.net) (or your enterprise dashboard).
2. Open **Manage → Organization**.
3. Scroll to the **Danger Zone** section.
4. Click **Delete Organization**.
5. Type the organization's slug **exactly** into the confirmation field. The match is case-sensitive and whitespace-sensitive — this is the irreversibility signal, the same pattern GitHub uses for destructive actions.
6. Click **Delete Organization** in the dialog.
You'll be signed out and redirected once the deletion completes.
## What gets destroyed
The delete runs as a single atomic operation across both the enterprise and OSS schemas. Everything below is purged in one request:
- Every **memory** in every tenant under the org
- Every **entity**, **relation**, **agent**, **document**, and **analysis report**
- Every **API key** (tenant-scoped and agent-scoped) and **install credential**
- Every **member** and pending **invite**
- **Audit logs** scoped to the org (the org-level deletion audit row survives — see below)
- **Organization settings** (security audit, lifecycle, scheduler config, etc.)
- All **tenants** under the org
- The **organization row** itself
The active **Paddle subscription** (if any) is cancelled as part of the same operation.
The only thing that survives is a single row in the `org_deletion_audit` table recording the action, your user id, and the per-table deletion counts. This is for forensic and compliance use and contains no memory content.
## Why no grace window?
Customer-initiated deletes are atomic by design. The slug-typed confirmation is the deliberate friction; once you click Delete, the org and its data are gone in seconds.
Operator-initiated deletes (when Caura support needs to delete an org on your behalf — a paid plan downgrade, ToS investigation, abuse case) **do** use a grace window: the org is soft-deleted first, your data is preserved for the grace period (30 days by default), and the org can be restored during that window. After the window elapses, a sweep purges it permanently.
Customer self-delete intentionally skips that window — you typed the slug, you meant it.
## I changed my mind
If you've **already clicked Delete and confirmed**, the data is gone. There is no restore for customer self-delete. Don't ask support — they can't recover it either.
If you **haven't confirmed yet**, just close the dialog. Nothing is destroyed until you complete the confirmation step.
## Data export before deleting
If you want to keep a copy of your memories before deleting, use the [REST API](/docs/api-reference) to export them first:
```bash
curl https://memclaw.net/api/v1/memories?tenant_id=YOUR_TENANT_ID \
-H "Authorization: Bearer $YOUR_ACCESS_TOKEN" > memclaw-export.json
```
Repeat per tenant if you have multiple. There is no built-in "download everything" button today — file a feature request if you'd find one useful.
## GDPR / right to erasure
This flow satisfies GDPR Article 17 (right to erasure / "right to be forgotten") for the organization and every tenant under it. The audit row that survives contains no personal data — only the action, the user id of the requester, the timestamp, and counts.
If you need a formal data-deletion confirmation for compliance, [contact support](mailto:support@caura.ai) after deleting and we can issue one against the audit record.
## On-prem and self-hosted
Both the OSS bundle ([self-host](/docs/getting-started/self-host)) and the [on-prem Enterprise](/docs/getting-started/on-prem) deployment expose the same Danger Zone in their bundled dashboard. The destroyed-data scope is identical; the Paddle cancel step is a no-op (you're not billed by Caura on those tiers).
# Your First Memory
URL: https://memclaw.net/docs/getting-started/first-write
Description: Write a memory and recall it — the smallest end-to-end flow.
A memory is natural-language `content` plus structured fields the platform infers asynchronously (entities, embeddings, etc.). Code blocks below auto-target this deployment's host.
## Write
{`curl -X POST "{HOST}/api/v1/memories" \\
-H "X-API-Key: mc_xxx" \\
-H "Content-Type: application/json" \\
-d '{
"tenant_id": "YOUR_TENANT_ID",
"agent_id": "my-agent",
"content": "The user prefers concise answers and dark mode."
}'`}
The response shape (and the exact set of returned fields) is defined in the OpenAPI spec — see the [API reference](/docs/api-reference/write).
## Recall
{`curl -X POST "{HOST}/api/v1/recall" \\
-H "X-API-Key: mc_xxx" \\
-H "Content-Type: application/json" \\
-d '{ "tenant_id": "YOUR_TENANT_ID", "query": "UI preferences" }'`}
Recall is hybrid (semantic + keyword) — the [memory pipeline](/docs/concepts/memory-pipeline) page covers what the search blends together. For non-semantic browse use `memclaw_list`; for read-by-id use `memclaw_manage op=read`.
# Enterprise — SaaS
URL: https://memclaw.net/docs/getting-started/managed
Description: The hosted MemClaw Enterprise deployment at memclaw.net. Caura runs the database, API, and worker for you.
The fastest way to start. Caura runs the platform; you only need an account and an API key.
## Sign up
1. Go to [memclaw.net/signin](https://memclaw.net/signin) and create an account.
2. Pick a plan from the [pricing page](https://memclaw.net/pricing).
3. Open **Settings → API Keys** and create a tenant-scoped key. It starts with `mc_`.
## Connect
Continue with the [Quickstart](/docs/getting-started/quickstart) — pick OpenClaw, MCP, or REST.
## Going beyond personal use: per-agent keys
The tenant-scoped `mc_` key is fine for a single user or one-off scripts. Any production deployment (a fleet of agents, a multi-user team, anything that needs trust gating or per-agent keystones) should bind each agent to its own **agent-scoped credential**. Both kinds use the `mc_` prefix on the wire — scope is bound at mint time on the credential row. Mint atomically:
```bash
curl -X POST https://memclaw.net/api/v1/admin/agent-keys/provision \
-H "X-API-Key: $MC_TENANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "your-agent-id",
"label": "human-readable label",
"initial_trust": 1,
"initial_fleet": "your-fleet-id"
}'
```
One round trip mints the credential, creates the Agent row, sets the trust level, and assigns the fleet. The response includes `raw_key` (returned once — save it) and `agent_row_created: true`. Verify with `GET /api/v1/whoami` using the new credential. Full flow in [Per-agent keys](/docs/integrations/per-agent-keys).
## When the managed deployment fits
- **Zero ops.** Caura runs Postgres, the worker, embeddings, and the LLM provider. No on-call rotation, no upgrades, no capacity planning on your side.
- **Production SLAs and support.** Uptime SLA, business-hours support on every plan, 24/7 + dedicated Slack on Business tier, and a named technical contact above that. See the [pricing page](https://memclaw.net/pricing) for current plan terms.
- **Security and compliance out of the box.** SOC 2 controls, audit logs, encryption at rest and in transit, tenant isolation enforced server-side. No security review of your own stack needed.
- **Fleet-level governance without wiring.** Trust levels, keystones, contradiction surfacing, the Karpathy Loop, and the Memory Crystallizer all run for you — flip the switches in the dashboard.
- **A working dashboard from day one** (Prism) for browsing, curating, and auditing memories. Plus billing, members, agent keys, and tenant management.
- **Upgrades and migrations are our problem.** New tools, model swaps, and schema migrations roll out on our side without downtime on yours.
## When self-host or on-prem fits better
- **Data residency or air-gap requirements.** If your data legally can't leave your VPC, region, or facility, see [On-prem Enterprise](/docs/getting-started/on-prem).
- **You're already running a Postgres-heavy stack** and want to keep one operational footprint. The OSS bundle is Apache 2.0 — see [OSS Self-host](/docs/getting-started/self-host).
- **Hobby / single-machine usage.** OSS on a laptop is free and complete.
Same code across all three paths — switching later is a config + migration, not a rewrite.
# On-Prem Enterprise
URL: https://memclaw.net/docs/getting-started/on-prem
Description: MemClaw Enterprise on a customer-managed VM — connected or fully air-gapped — with a license-gated Docker Compose stack.
For customer-managed deployments inside your own VM, datacenter, or air-gapped environment, Caura ships a separate on-prem distribution that wraps the platform in a one-VM Docker Compose stack and a Caura-issued license file.
**Repository:** [github.com/caura-ai/memclaw-onprem](https://github.com/caura-ai/memclaw-onprem) — that repo's `README.md` and `docs/` are the canonical install / day-2 / upgrade reference. The summary below is a pointer.
## What it gives you
- A single-VM Docker Compose stack (`docker-compose.yml` in the repo).
- A signed `license.key` — required; the platform refuses to start without one.
- Connected (pulls from `ghcr.io/caura-ai/*`) **or** fully air-gapped (load images from a tarball — `airgap-load.sh`).
- Backup / restore scripts (`scripts/backup.sh`, `scripts/restore.sh`) covering Postgres, Redis, RabbitMQ, `.env`, and the license file.
- Tag-driven, reversible upgrades (`upgrade.sh`) on Docker named volumes.
- A redacted support-bundle workflow for talking to Caura support.
## Quickstart — connected VM
```bash
curl -sL https://onprem.caura.ai/install.sh | bash
```
The installer runs preflight checks, generates secrets, writes `/opt/memclaw/.env`, pulls pinned images, brings the stack up, and prints the URL of a first-run wizard where an admin uploads the license and creates the first account.
## Quickstart — air-gapped VM
```bash
# Transfer memclaw-onprem-.tar.gz to the VM (USB, SCP, etc.)
./airgap-load.sh /path/to/memclaw-onprem-.tar.gz
./install.sh --offline --license /path/to/license.key
```
The tarball bundles MemClaw services plus upstream bases (Postgres/pgvector, Redis, RabbitMQ), so `docker compose` never needs to pull.
## Silent / unattended install
Every value can be supplied via CLI flag, `MEMCLAW_*` env var, or a single config file:
```bash
./install.sh --config /etc/memclaw/install.conf --non-interactive
```
See [`install.conf.example`](https://github.com/caura-ai/memclaw-onprem/blob/main/install.conf.example) for the full template.
## Prerequisites
- Ubuntu 24.04 or equivalent.
- Docker ≥ 24 and Docker Compose v2.
- **16 GB RAM minimum**, **200 GB disk** for Postgres + Redis + RabbitMQ + room for memory growth.
- A DNS record pointing to the VM and a TLS cert (`cert.pem` + `key.pem`) for that hostname.
- A Caura-issued `license.key`.
## When on-prem is the right pick
- You need a contract, signed builds, and SLAs.
- You require a fully air-gapped install with no calls home.
- You have data-residency or compliance requirements that rule out the managed deployment.
- You want platform-tier features (multi-tenant gateway, audit retention, SSO).
The OSS [Self-host](/docs/getting-started/self-host) is the right pick if you only need the engine and are happy operating it yourself under Apache 2.0.
## Day-2, upgrades, troubleshooting
Live in the on-prem repo:
- [`docs/install.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/install.md) — connected install
- [`docs/install-airgap.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/install-airgap.md) — air-gapped install
- [`docs/day2-ops.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/day2-ops.md) — backups, restore, log rotation
- [`docs/upgrade.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/upgrade.md) — sequential-minor upgrade path, rollback
- [`docs/troubleshooting.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/troubleshooting.md)
- [`docs/security.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/security.md), [`docs/TLS.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/TLS.md), [`docs/logging.md`](https://github.com/caura-ai/memclaw-onprem/blob/main/docs/logging.md)
## Pricing and evaluation
For pricing, sizing, and an evaluation copy of the on-prem distribution and license: [contact the Caura team](https://memclaw.net/about).
# Quickstart
URL: https://memclaw.net/docs/getting-started/quickstart
Description: Connect any AI agent to MemClaw in five minutes — pick managed, self-hosted, or REST.
You have three integration paths. Pick the one that matches your stack:
| Path | When to use | Setup time |
| --- | --- | --- |
| **OpenClaw** | You run a fleet of agents and want them all to share memory | ~3 min |
| **MCP** | A single client (Claude Desktop, Claude Code, Cursor, Windsurf) | ~1 min |
| **REST** | Custom code in any language | ~30 sec |
### Create an API key
For the **managed** deployment, sign in at [/signin](/signin) and generate a tenant-scoped key from **Settings → API Keys**. Keys start with `mc_`.
For **self-hosted OSS**, pick one auth mode in your `.env`:
```bash title=".env"
# Single-tenant, no key needed
IS_STANDALONE=true
# Or: multi-tenant, full access
ADMIN_API_KEY=admin_xxx
# Or: shared gate (key + X-Tenant-ID header)
MEMCLAW_API_KEY=mc_xxx
```
### Pick your path
Run on the gateway host:
{`curl -s -H "X-API-Key: $MEMCLAW_API_KEY" \\
"{HOST}/api/v1/install-plugin?fleet_id=YOUR_FLEET_ID" | bash`}
The installer downloads the plugin, builds it, edits `~/.openclaw/openclaw.json` to claim the `memory` slot, and writes the env file. Restart the gateway:
```bash
openclaw gateway restart
```
Paste this into your MCP client config (`claude_desktop_config.json`, `~/.cursor/config.json`, etc.):
{`{
"mcpServers": {
"memclaw": {
"url": "{HOST}/mcp",
"headers": { "X-API-Key": "mc_xxx" }
}
}
}`}
Restart the client. The `memclaw_*` tools become available.
For anything beyond a single user — a team or fleet of agents — bind each agent to its own **agent-scoped credential** instead of sharing the tenant-scoped key. See [Per-agent keys](/docs/integrations/per-agent-keys) for the atomic-provisioning flow that mints the credential, the Agent row, and trust level in one call. Both kinds use the `mc_` prefix on the wire — scope is bound at mint time on the credential row.
No install. Hit the API directly from any language:
{`curl -X POST "{HOST}/api/v1/memories" \\
-H "X-API-Key: mc_xxx" \\
-H "Content-Type: application/json" \\
-d '{ "tenant_id": "YOUR_TENANT_ID", "content": "User prefers dark mode." }'`}
### Verify
{`curl "{HOST}/api/v1/health" -H "X-API-Key: mc_xxx"
# {"status": "ok", "storage": "connected", "redis": "connected", "event_bus": "ok"}`}
A `503` means a required dependency is unhealthy (storage / redis / event_bus). For OpenClaw installs, your node should appear in **Fleet Management** within 60 seconds of the first heartbeat (`HEARTBEAT_INTERVAL_SECONDS = 60` per `core-api/src/core_api/constants.py`).
### Write your first memory
For OpenClaw and MCP, just talk to your agent:
> Remember that I prefer concise answers and dark mode.
Then ask later:
> What do you know about my UI preferences?
The agent calls `memclaw_write` and `memclaw_recall` automatically. The exact write response shape is defined by the `MemoryOut` schema in `core-api/src/core_api/schemas.py` (id, tenant_id, fleet_id, agent_id, memory_type, title, content, weight, entity_links, status, …).
## Next
- [Memory pipeline concepts](/docs/concepts/memory-pipeline)
- [Per-platform integration walkthroughs](/docs/integrations/claude-code)
- [Full API reference](/docs/api-reference)
# OSS
URL: https://memclaw.net/docs/getting-started/self-host
Description: Run the open-source MemClaw on your own infrastructure with Docker. Apache 2.0.
The OSS bundle at [github.com/caura-ai/caura-memclaw](https://github.com/caura-ai/caura-memclaw) ships everything you need: API, worker, plugin, and a Postgres + pgvector schema. Apache 2.0.
## Prerequisites
- Docker 24+ and Docker Compose v2
- 4 GB RAM, 2 vCPU minimum
- An entity-extraction provider key for enrichment — one of: **OpenAI**, **Anthropic**, **Gemini**, or **OpenRouter** (per [`.env.example`](https://github.com/caura-ai/caura-memclaw/blob/main/.env.example) provider matrix). Anthropic/Gemini/OpenRouter pair with OpenAI for embeddings since they don't expose embedding APIs. Vertex AI is platform-tier only (managed deployment).
## Bring it up
```bash
git clone https://github.com/caura-ai/caura-memclaw
cd caura-memclaw
cp .env.example .env # set IS_STANDALONE=true, EMBEDDING_PROVIDER=, ENTITY_EXTRACTION_PROVIDER=, OPENAI_API_KEY=…
docker compose up -d
```
The API listens on `:8000`, the worker runs in the background, and migrations apply on first boot. You can now follow the [Quickstart](/docs/getting-started/quickstart) and pick MCP or REST.
## Auth modes
Pick one — they are mutually exclusive:
- `IS_STANDALONE=true` — no auth, single tenant. Fine for laptops, fine for trusted networks.
- `ADMIN_API_KEY=...` — full access with one key. Multi-tenant.
- `MEMCLAW_API_KEY=...` + `X-Tenant-ID` header — shared gate, one key per tenant.
## Going further
- [Architecture overview](/docs/concepts/architecture)
- [Environment variables reference](/docs/reference/env-vars)
- [Contribute on GitHub](https://github.com/caura-ai/caura-memclaw/blob/main/CONTRIBUTING.md)
# MemClaw Documentation
URL: https://memclaw.net/docs
Description: Persistent, governed memory for AI agent fleets. Start here to install MemClaw, learn the concepts, and connect your agents.
MemClaw gives AI agent fleets a persistent, governed, shared memory so agents learn from each other and get smarter over time. These docs cover everything for **operators and developers**. If you are building an agent that *uses* MemClaw, jump to the [For Agents](/docs/agents) tab.
## Three ways in
## Looking for something specific?
- **Self-hosting (OSS)**: [Self-host with Docker](/docs/getting-started/self-host)
- **On-prem (managed for your perimeter)**: [On-prem deployment](/docs/getting-started/on-prem)
- **Claude Code / Claude Desktop / Cursor**: [Integrations](/docs/integrations/claude-code)
- **OpenClaw plugin**: [OpenClaw integration](/docs/integrations/openclaw)
- **Contributing**: [GitHub repo](https://github.com/caura-ai/caura-memclaw)
# AutoGen
URL: https://memclaw.net/docs/integrations/autogen
Description: Give a Microsoft AutoGen agent persistent, governed memory with MemClaw over MCP.
MemClaw is [MCP-native](/docs/integrations/mcp), so an AutoGen `AssistantAgent`
can load the full `memclaw_*` tool surface through
[`autogen-ext`](https://microsoft.github.io/autogen/stable/reference/python/autogen_ext.tools.mcp.html)'s
MCP tools.
```bash
pip install "autogen-agentchat" "autogen-ext[openai,mcp]"
```
{`import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_ext.tools.mcp import StreamableHttpServerParams, mcp_server_tools
async def main():
server_params = StreamableHttpServerParams(
url="{HOST}/mcp",
# Use an agent-scoped key in production (see Per-agent keys below).
headers={"X-API-Key": "mc_xxx"},
)
tools = await mcp_server_tools(server_params) # all memclaw_* tools
agent = AssistantAgent(
name="assistant",
model_client=OpenAIChatCompletionClient(model="gpt-4o"),
tools=tools,
)
print(await agent.run(task="Remember our Q3 target is $4M, then recall it."))
asyncio.run(main())`}
Always bind a real `agent_id` to the credential. For anything beyond a
prototype, give each agent its own agent-scoped key — see
[Per-agent keys](/docs/integrations/per-agent-keys) — so trust gating, fleet
membership, and per-agent keystones apply.
## Prefer REST?
Without MCP, wrap MemClaw's [`POST /api/v1/memories`](/docs/api-reference/write)
and [`POST /api/v1/recall`](/docs/api-reference/recall) as AutoGen
`FunctionTool`s. See [REST](/docs/integrations/rest) for the request shapes.
# Claude Code
URL: https://memclaw.net/docs/integrations/claude-code
Description: Wire MemClaw into Anthropic's Claude Code CLI.
Claude Code supports MCP out of the box. Two minutes total.
## Install
Add a `.mcp.json` at the repo root (or to your global Claude Code settings):
{`{
"mcpServers": {
"memclaw": {
"url": "{HOST}/mcp",
"headers": { "X-API-Key": "mc_xxx" }
}
}
}`}
Restart Claude Code. The `memclaw_*` tools become available — Claude Code will surface them to you on first use and ask permission like any other tool.
## Going to production: use a per-agent key
The `mc_` quickstart key above is a tenant-scoped credential — fine for personal use. Anything that ships a fleet of agents should bind each agent to its own **agent-scoped credential** for trust gating, fleet membership, and per-agent keystones. The MCP server accepts the credential on either `X-API-Key: mc_…` or `Authorization: Bearer mc_…` — both tenant-scoped and agent-scoped credentials share the `mc_` prefix; scope is set at mint time. See [Per-agent keys](/docs/integrations/per-agent-keys) for the atomic provisioning flow.
## Using it
Just talk to the model:
> Remember that this repo uses pnpm, not npm.
> What did we decide about the auth flow?
The tool calls happen automatically. See the full tool surface in the agent skill at [/docs/agents](/docs/agents).
## Tips
- Use a per-fleet API key so memories from different projects stay separate. Set `tenant_id` to the project name.
- The Caura `memclaw` skill (`~/.claude/skills/memclaw`) ships richer prompting. Install it:
# Claude Desktop
URL: https://memclaw.net/docs/integrations/claude-desktop
Description: Add MemClaw as an MCP server in Claude Desktop.
Edit your Claude Desktop config:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
{`{
"mcpServers": {
"memclaw": {
"url": "{HOST}/mcp",
"headers": { "X-API-Key": "mc_xxx" }
}
}
}`}
Quit and re-open Claude Desktop. Confirm the `memclaw` server is connected from **Settings → Developer**.
The `memclaw_*` tools now appear in any conversation. Cross-conversation persistence is the headline benefit: ask Claude Desktop to "remember X" in one chat, then start a new chat tomorrow and ask "what do you know about X?".
## Going to production: use a per-agent key
The `mc_` quickstart key above is a tenant-scoped credential — fine for a single user. For a team or fleet, bind each agent to its own **agent-scoped credential** for trust gating, fleet membership, and per-agent keystones. Both kinds use the `mc_` prefix on the wire; scope is set at mint time. See [Per-agent keys](/docs/integrations/per-agent-keys).
# CrewAI
URL: https://memclaw.net/docs/integrations/crewai
Description: Give a CrewAI agent persistent, governed memory with MemClaw over MCP.
MemClaw is [MCP-native](/docs/integrations/mcp), so a CrewAI agent can pick up
the full `memclaw_*` tool surface through `crewai-tools`'
[`MCPServerAdapter`](https://docs.crewai.com/en/mcp/overview).
```bash
pip install crewai "crewai-tools[mcp]"
```
{`from crewai import Agent
from crewai_tools import MCPServerAdapter
server_params = {
"url": "{HOST}/mcp",
"transport": "streamable-http",
# Use an agent-scoped key in production (see Per-agent keys below).
"headers": {"X-API-Key": "mc_xxx"},
}
# MCPServerAdapter is a context manager — it opens the connection and
# exposes the memclaw_* tools for the duration of the block.
with MCPServerAdapter(server_params) as mcp_tools:
agent = Agent(
role="Memory-backed assistant",
goal="Remember and recall facts across sessions.",
backstory="I persist what I learn to MemClaw and recall it on demand.",
tools=mcp_tools,
verbose=True,
)`}
Note the transport string is `streamable-http` (with a hyphen) here — that's
the value `crewai-tools` expects.
Always bind a real `agent_id` to the credential. For anything beyond a
prototype, give each agent its own agent-scoped key — see
[Per-agent keys](/docs/integrations/per-agent-keys) — so trust gating, fleet
membership, and per-agent keystones apply.
## Prefer REST?
If you don't want to run an MCP client, wrap MemClaw's
[`POST /api/v1/memories`](/docs/api-reference/write) and
[`POST /api/v1/recall`](/docs/api-reference/recall) as CrewAI tools with the
`@tool` decorator from `crewai.tools`. See [REST](/docs/integrations/rest) for
the request shapes.
# Cursor
URL: https://memclaw.net/docs/integrations/cursor
Description: Wire MemClaw into Cursor's MCP support.
Open Cursor settings → **MCP** → **Add New Server**, or edit `~/.cursor/config.json` directly:
{`{
"mcpServers": {
"memclaw": {
"url": "{HOST}/mcp",
"headers": { "X-API-Key": "mc_xxx" }
}
}
}`}
Restart Cursor. The `memclaw_*` tools become available in chat and inside Composer.
Set `tenant_id` to the project / workspace name so different repos keep their memories isolated. Cursor doesn't surface tool calls as prominently as Claude Code — if the model isn't using MemClaw, ask it explicitly to "use the memclaw tools to remember/recall…".
## Going to production: use a per-agent key
The `mc_` quickstart key above is a tenant-scoped credential — fine for personal use. Anything that ships a team or fleet should bind each agent to its own **agent-scoped credential** for trust gating, fleet membership, and per-agent keystones. Both kinds use the `mc_` prefix on the wire; scope is set at mint time. See [Per-agent keys](/docs/integrations/per-agent-keys).
# LangChain
URL: https://memclaw.net/docs/integrations/langchain
Description: Give a LangChain or LangGraph agent persistent, governed memory with MemClaw — over MCP or the REST API.
MemClaw is framework-agnostic: it speaks [MCP](/docs/integrations/mcp) and plain
[REST](/docs/integrations/rest). There are two clean ways to give a LangChain
(or LangGraph) agent long-term memory with it.
## Option A — over MCP (recommended)
MemClaw is MCP-native, so the simplest path is to load its `memclaw_*` tools
straight into your agent with
[`langchain-mcp-adapters`](https://github.com/langchain-ai/langchain-mcp-adapters).
Your agent gets the full surface — `memclaw_write`, `memclaw_recall`,
`memclaw_manage`, and the rest — with no glue code per tool.
```bash
pip install langchain-mcp-adapters langgraph "langchain[openai]"
```
{`import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
client = MultiServerMCPClient({
"memclaw": {
"url": "{HOST}/mcp",
"transport": "streamable_http",
# Use an agent-scoped key in production (see Per-agent keys below).
"headers": {"X-API-Key": "mc_xxx"},
}
})
async def main():
tools = await client.get_tools() # all memclaw_* tools
agent = create_react_agent("openai:gpt-4o", tools)
result = await agent.ainvoke({
"messages": [{"role": "user", "content": "Remember that our Q3 revenue target is $4M, then tell me what it is."}]
})
print(result["messages"][-1].content)
asyncio.run(main())`}
## Option B — over REST (no MCP)
If you'd rather not run an MCP client, wrap MemClaw's two core endpoints —
[`POST /api/v1/memories`](/docs/api-reference/write) and
[`POST /api/v1/recall`](/docs/api-reference/recall) — as ordinary LangChain
tools.
```bash
pip install langchain langchain-openai requests
```
{`import requests
from langchain_core.tools import tool
MEMCLAW_URL = "{HOST}"
HEADERS = {"X-API-Key": "mc_xxx"}
TENANT = "my-team"
@tool
def remember(content: str) -> str:
"""Persist a fact to long-term memory for later recall."""
r = requests.post(
f"{MEMCLAW_URL}/api/v1/memories",
headers=HEADERS,
json={"tenant_id": TENANT, "agent_id": "langchain-agent", "content": content},
)
r.raise_for_status()
return "saved"
@tool
def recall(query: str) -> str:
"""Search long-term memory for facts relevant to the query."""
r = requests.post(
f"{MEMCLAW_URL}/api/v1/recall",
headers=HEADERS,
json={"tenant_id": TENANT, "query": query},
)
r.raise_for_status()
return r.text`}
Bind the tools to any LangChain agent or LangGraph `create_react_agent` the same
way you would any other tool:
```python
from langgraph.prebuilt import create_react_agent
agent = create_react_agent("openai:gpt-4o", [remember, recall])
```
Always pass a real `agent_id` (the REST example uses `"langchain-agent"`). The
reserved default identity is rejected server-side, so writes silently no-op
without one. For anything beyond a prototype, give each agent its own
agent-scoped credential — see [Per-agent keys](/docs/integrations/per-agent-keys) —
so trust gating, fleet membership, and per-agent keystones apply.
## Where memory lives
Both paths write to the same governed store: every memory is enriched, scoped,
and audit-logged. Recall returns the relevant slice rather than the full
history, which is what keeps token cost flat as the conversation grows. See
[Memory Pipeline](/docs/concepts/memory-pipeline) for what happens between
`write` and a memory becoming recallable.
# LlamaIndex
URL: https://memclaw.net/docs/integrations/llamaindex
Description: Give a LlamaIndex agent persistent, governed memory with MemClaw over MCP.
MemClaw is [MCP-native](/docs/integrations/mcp), so a LlamaIndex `FunctionAgent`
can load the full `memclaw_*` tool surface through the
[`llama-index-tools-mcp`](https://llamahub.ai/l/tools/llama-index-tools-mcp)
package.
```bash
pip install llama-index llama-index-tools-mcp llama-index-llms-openai
```
{`from llama_index.tools.mcp import BasicMCPClient, McpToolSpec
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI
# A "/mcp" URL uses the Streamable HTTP transport.
mcp_client = BasicMCPClient(
"{HOST}/mcp",
# Use an agent-scoped key in production (see Per-agent keys below).
headers={"X-API-Key": "mc_xxx"},
)
async def main():
tools = await McpToolSpec(client=mcp_client).to_tool_list_async()
agent = FunctionAgent(
tools=tools,
llm=OpenAI(model="gpt-4o"),
system_prompt="You can persist and recall facts with MemClaw.",
)
print(await agent.run("Remember our Q3 revenue target is $4M, then recall it."))`}
Always bind a real `agent_id` to the credential. For anything beyond a
prototype, give each agent its own agent-scoped key — see
[Per-agent keys](/docs/integrations/per-agent-keys) — so trust gating, fleet
membership, and per-agent keystones apply.
## Prefer REST?
Without MCP, wrap MemClaw's [`POST /api/v1/memories`](/docs/api-reference/write)
and [`POST /api/v1/recall`](/docs/api-reference/recall) as LlamaIndex tools with
`FunctionTool.from_defaults` (from `llama_index.core.tools`). See
[REST](/docs/integrations/rest) for the request shapes.
# Generic MCP
URL: https://memclaw.net/docs/integrations/mcp
Description: Connect any MCP-compatible client (Windsurf, Cline, Continue, JetBrains MCP, …) to MemClaw.
Any client that implements [Model Context Protocol](https://modelcontextprotocol.io) can use MemClaw. The MCP server is mounted directly on core-api at `/mcp`. Use the URL of whatever deployment you're connecting to:
- **Managed:** `https://memclaw.net/mcp`
- **Staging:** `https://memclaw.net/mcp`
- **Self-hosted:** `http://localhost:8000/mcp` (or wherever your core-api is reachable)
## Standard MCP server entry
The snippet below auto-targets this deployment's host.
{`{
"mcpServers": {
"memclaw": {
"url": "{HOST}/mcp",
"headers": { "X-API-Key": "mc_xxx" }
}
}
}`}
This is the HTTP / Streamable-HTTP transport. Most modern MCP clients (Claude Code, Claude Desktop, Cursor, Windsurf, Cline, Continue) support it.
## Tools exposed
The full `memclaw_*` tool list lives in [`/docs/agents`](/docs/agents) (rendered verbatim from upstream [`SKILL.md`](https://github.com/caura-ai/caura-memclaw/blob/main/static/skills/memclaw/SKILL.md)). Current surface from the OSS source:
`memclaw_recall` · `memclaw_write` · `memclaw_manage` · `memclaw_list` · `memclaw_doc` · `memclaw_entity_get` · `memclaw_tune` · `memclaw_insights` · `memclaw_evolve` · `memclaw_stats` · `memclaw_keystones` · `memclaw_keystones_set`
For exact parameter schemas, fetch [`/api/v1/tool-descriptions`](/api/v1/tool-descriptions) — that endpoint is generated from the same ToolSpec registry the MCP server uses.
## Long-lived integrations: per-agent keys
The single tenant-scoped `mc_` flow above is fine for one-off setups. For anything that ships — trust gating, fleet membership, per-agent keystones, real identity in the dashboard — bind each agent to its own **agent-scoped credential** via [Integrations → Per-agent keys](/docs/integrations/per-agent-keys). The MCP server accepts the credential on either `X-API-Key: mc_…` or `Authorization: Bearer mc_…` — both tenant-scoped and agent-scoped credentials share the `mc_` prefix; scope is set at mint time. *(Pre-existing `mca_…` keys continue to authenticate via back-compat.)*
## Reading MCP tool results
Every MCP tool result has an `isError` boolean plus a `content` array. On gateway-side refusals (FORBIDDEN, INVALID_ARGUMENTS, NOT_FOUND, etc.) the server returns `isError=True` with a JSON `{"error": {"code": ..., "message": ..., "details": {...}}}` envelope in `content[0].text`. Always inspect `result.isError` before treating `content` as success — the structured envelope tells you exactly what was rejected and why.
# OpenAI Agents SDK
URL: https://memclaw.net/docs/integrations/openai-agents
Description: Give an OpenAI Agents SDK agent persistent, governed memory with MemClaw over MCP.
MemClaw is [MCP-native](/docs/integrations/mcp), so the
[OpenAI Agents SDK](https://openai.github.io/openai-agents-python/mcp/) can
attach the full `memclaw_*` tool surface with `MCPServerStreamableHttp`.
```bash
pip install openai-agents
```
{`import asyncio
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async def main():
async with MCPServerStreamableHttp(
name="MemClaw",
params={
"url": "{HOST}/mcp",
# Use an agent-scoped key in production (see Per-agent keys below).
"headers": {"X-API-Key": "mc_xxx"},
},
cache_tools_list=True,
) as memclaw:
agent = Agent(
name="Assistant",
instructions="Persist and recall facts with MemClaw.",
mcp_servers=[memclaw],
)
result = await Runner.run(agent, "Remember our Q3 target is $4M, then recall it.")
print(result.final_output)
asyncio.run(main())`}
Always bind a real `agent_id` to the credential. For anything beyond a
prototype, give each agent its own agent-scoped key — see
[Per-agent keys](/docs/integrations/per-agent-keys) — so trust gating, fleet
membership, and per-agent keystones apply.
## Prefer REST?
Without MCP, wrap MemClaw's [`POST /api/v1/memories`](/docs/api-reference/write)
and [`POST /api/v1/recall`](/docs/api-reference/recall) as function tools with
the `@function_tool` decorator from `agents`. See
[REST](/docs/integrations/rest) for the request shapes.
# OpenClaw
URL: https://memclaw.net/docs/integrations/openclaw
Description: Drop MemClaw into an OpenClaw gateway and every agent in the fleet inherits shared memory.
OpenClaw is the agent gateway / orchestrator. The MemClaw plugin claims its `memory` slot — meaning every agent that runs through the gateway gets the same `memclaw_*` tools, the same backing store, and the same governance rules, with no per-agent setup.
## Install
On the gateway host (URL auto-targets this deployment):
{`curl -s -H "X-API-Key: $MEMCLAW_API_KEY" \\
"{HOST}/api/v1/install-plugin?fleet_id=$FLEET_ID" | bash`}
What this does (manual fallback if you prefer):
1. Downloads plugin source files into `~/.openclaw/plugins/memclaw/`
2. Runs `npm install && npm run build`
3. Edits `~/.openclaw/openclaw.json` to:
- Claim the `memory` slot
- Disable the default `memory-core` plugin
- Allowlist the `memclaw_*` tools
4. Writes `MEMCLAW_API_KEY`, `MEMCLAW_FLEET_ID`, and `MEMCLAW_API_URL` into the plugin's `.env`
Then restart the gateway:
```bash
openclaw gateway restart
# or: systemctl --user restart openclaw-gateway
```
## Verify
Within 60 seconds, the gateway should appear in the **Fleet Management** page on your MemClaw dashboard. From there you can monitor heartbeats, trust levels, and per-agent memory writes.
## Why use the gateway path
- One plugin install covers an arbitrary number of agents
- Per-agent memory isolation is automatic (each agent gets its own `agent_id`)
- Fleet-wide trust and governance — the architect's memories stay above the junior agents'
- Heartbeat-based health monitoring + auto-disable on misbehavior
# Per-agent keys (no plugin)
URL: https://memclaw.net/docs/integrations/per-agent-keys
Description: Bootstrap a long-lived integration without the OpenClaw plugin — provision an agent-scoped credential, identify, and call MCP.
If you're building a Python / Node / Go integration directly against MemClaw — without the OpenClaw plugin runtime — bind every long-lived agent to its own **agent-scoped credential** rather than calling under the tenant-scoped key. Agent-scoped credentials give you trust gating, fleet membership, per-agent keystones, and a real identity in the dashboard.
Both kinds of credential use the `mc_` prefix on the wire; scope (tenant vs agent) is bound at mint time on the credential row, not encoded in the prefix. *(Pre-existing `mca_…` keys continue to authenticate via back-compat.)*
The single tenant-scoped flow on the [REST](/docs/integrations/rest) and [MCP](/docs/integrations/mcp) pages is fine for one-off scripts; the four-step flow here is for anything that ships.
## 1. Mint an agent-scoped credential
`POST /api/v1/admin/agent-keys/provision` mints an agent-scoped credential **and** creates the Agent row eagerly, so a follow-up `PATCH /agents/{id}/trust` works in the same session without a synthetic first write.
{`curl -X POST "{HOST}/api/v1/admin/agent-keys/provision" \\
-H "X-API-Key: $MC_TENANT_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"agent_id": "quote-agent-na",
"label": "north-america CRM",
"initial_trust": 1,
"initial_fleet": "na-sales"
}'`}
Response:
```json
{
"id": "…",
"tenant_id": "…",
"agent_id": "quote-agent-na",
"raw_key": "mc_…",
"agent_row_created": true,
"created_at": "…"
}
```
Save `raw_key` immediately — it is only returned once. `agent_row_created: true` confirms the Agent row exists; `initial_trust` and `initial_fleet` were applied in the same round-trip.
Request body fields:
- `agent_id` (required) — stable identifier for this agent. Surfaces in `/whoami`, audit, and per-agent dashboards.
- `label` — human-readable hint in the dashboard.
- `initial_trust` — `0` read-only / `1` write to home fleet / `2` cross-fleet read / `3` cross-fleet write + delete. Default `1`.
- `initial_fleet` — fleet membership; omit for tenant-wide scope.
- `display_name` — human-readable name surfaced on the dashboard.
## 2. Verify identity (`/whoami`)
Before any real tool call, confirm MemClaw resolves your credentials the way you expect:
{`curl "{HOST}/api/v1/whoami" \\
-H "X-API-Key: $AGENT_KEY"`}
```json
{
"tenant_id": "your-tenant-id",
"agent_id": "quote-agent-na",
"auth_source": "gateway-header",
"via_gateway": true
}
```
If `agent_id` is `null` you're sending a tenant-scoped credential, not an agent-scoped one — the gateway only injects `X-Agent-ID` on the agent-scoped path. (Both kinds share the `mc_` prefix; the dashboard "Kind" column tells you which is which.)
## 3. Open an MCP session
MemClaw speaks MCP streamable-http at `/mcp` (both `/mcp` and `/mcp/` work). The server accepts the API key on either header — use whichever your SDK supports:
| Header | When to use |
| --- | --- |
| `X-API-Key: mc_…` | Canonical. Use if you control the request shape. |
| `Authorization: Bearer mc_…` | OAuth-style. Required by Anthropic's remote-MCP integration and SDKs that only emit `Authorization` headers. |
Dashboard-issued JWTs are also accepted via `Authorization: Bearer `; the server tries JWT decode first.
### Python (the `mcp` library)
{`import asyncio
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
headers = {"X-API-Key": "mc_..."} # agent-scoped credential
url = "{HOST}/mcp/"
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool(
"memclaw_write",
{"content": "First memory from the Python harness."},
)
# Always inspect both result.isError and the content envelope.
# Gateway-side refusals (FORBIDDEN, INVALID_ARGUMENTS, NOT_FOUND, ...)
# arrive as isError=True with a JSON {"error":{...}} body — naive
# clients that only check content[0].text for "id" will silently
# treat failure as success.
if result.isError:
print("tool refused:", result.content[0].text)
else:
print(result)
asyncio.run(main())`}
### Anthropic SDK (remote-MCP)
{`from anthropic import Anthropic
client = Anthropic()
msg = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=[{"role": "user", "content": "Save this note: pricing meets 10 May."}],
extra_body={
"mcp_servers": [
{
"type": "url",
"url": "{HOST}/mcp/",
"name": "memclaw",
"authorization_token": "mc_...", # agent-scoped credential
}
]
},
)
print(msg.content)`}
The SDK forwards `authorization_token` as `Authorization: Bearer mc_…`. MemClaw recognises that shape and resolves tenant + agent identity from it.
## 4. Elevate trust (when needed)
If you set `initial_trust` in step 1, skip this. Otherwise:
{`curl -X PATCH "{HOST}/api/v1/agents/quote-agent-na/trust?tenant_id=$TENANT_ID" \\
-H "X-API-Key: $MC_TENANT_KEY" \\
-H "Content-Type: application/json" \\
-d '{"trust_level": 2}'`}
Trust levels:
- `0` — read-only.
- `1` — write to home fleet.
- `2` — cross-fleet read.
- `3` — cross-fleet write + delete + update others' memories.
## End-to-end bootstrap, one block
{`TENANT_KEY=mc_...
AGENT_ID="quote-agent-na"
FLEET_ID="na-sales"
# 1. Provision agent + Agent row + trust + fleet in one call.
RESP=$(curl -s -X POST "{HOST}/api/v1/admin/agent-keys/provision" \\
-H "X-API-Key: $TENANT_KEY" \\
-H "Content-Type: application/json" \\
-d "{\\"agent_id\\":\\"$AGENT_ID\\",\\"initial_trust\\":1,\\"initial_fleet\\":\\"$FLEET_ID\\"}")
AGENT_KEY=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['raw_key'])")
# 2. Verify.
curl -s "{HOST}/api/v1/whoami" -H "X-API-Key: $AGENT_KEY"
# 3. Use.
curl -s "{HOST}/api/v1/memories" \\
-H "X-API-Key: $AGENT_KEY" \\
-H "Content-Type: application/json" \\
-d "{\\"agent_id\\":\\"$AGENT_ID\\",\\"fleet_id\\":\\"$FLEET_ID\\",\\"content\\":\\"Hello world\\"}"`}
Four steps, one round-trip per agent. No provision → fake-write → patch-trust dance.
## Idempotency
A write of identical content (same `agent_id`, same `fleet_id`) is retry-safe via MCP:
- First call → `201` with the new memory id.
- Identical retry → `200` with `{ "status": "duplicate", "existing_id": "…" }`.
Cross-agent writes of identical content no longer collide — each agent gets its own record.
## Authoring keystones
When you want an agent to author governance rules ([keystones](/docs/concepts/keystones)), use `memclaw_keystones_set` over MCP:
```python
result = await session.call_tool(
"memclaw_keystones_set",
{
"op": "set",
"doc_id": "no-rollback-to-cve-versions",
"title": "Refuse rollback to CVE versions",
"content": "Never recommend rolling back to a version listed in any CVE entry.",
"scope": "tenant",
"weight": "high",
},
)
```
Two gotchas worth knowing before you write the call:
- `agent_id` in `memclaw_keystones_set` names the **TARGET** agent the rule binds to, not the caller. Pass it only for `scope=agent`. For `scope=tenant` or `scope=fleet`, omit it — passing it returns `INVALID_ARGUMENTS`.
- The companion read tool `memclaw_keystones` returns the merged rule set under the JSON key `rules` (not `keystones`).
Trust gating is dynamic: self-authoring (`scope=agent` + target = caller) needs trust ≥ 1; anything else (fleet, tenant, or targeting another agent) needs trust ≥ 2.
## Common pitfalls
- **`POST /provision` returns the raw key once.** Save it before the response goes out of scope.
- **`PATCH /agents/{id}/trust` returns 404 immediately after provisioning.** Means the Agent row was not materialised atomically — should not happen on builds after 2026-05-14. Check `/whoami` and `GET /api/v1/agents/{id}`; if `agent_row_created: false` is in the provision response, the deployment is missing `CORE_STORAGE_API_URL`.
- **MCP tool result has `isError=False` but `content[0].text` is `{"error": …}`.** Pre-2026-05-15 builds left `isError` at the default for gateway-side refusals. On current builds, every structured-error envelope arrives with `isError=True`.
- **Streaming client hangs on `/mcp` (no slash).** Older builds redirected `/mcp` → `/mcp/`; current builds serve both paths in-process.
## Reference
- `POST /api/v1/admin/agent-keys/provision` — atomic provisioning (this guide).
- `GET /api/v1/whoami` — identity probe.
- `GET /api/v1/agents/{id}?tenant_id=…` — agent detail.
- `PATCH /api/v1/agents/{id}/trust?tenant_id=…` — change trust level.
- `POST /api/v1/memories` — REST write (mirrors `memclaw_write` over MCP).
- `mcp://…/mcp/` — streamable-http MCP endpoint.
# REST
URL: https://memclaw.net/docs/integrations/rest
Description: Hit MemClaw directly from any language. No SDK required.
If your stack doesn't speak MCP, drive MemClaw over plain HTTP. All endpoints accept JSON over HTTPS, authenticated with the `X-API-Key` header.
## Example: write + recall
URLs in the snippets below auto-target this deployment.
{`# Write
curl -X POST "{HOST}/api/v1/memories" \\
-H "X-API-Key: mc_xxx" \\
-H "Content-Type: application/json" \\
-d '{
"tenant_id": "my-team",
"agent_id": "ingest-bot",
"content": "Q3 revenue target is $4M, set on 2026-04-15."
}'
# Recall
curl -X POST "{HOST}/api/v1/recall" \\
-H "X-API-Key: mc_xxx" \\
-H "Content-Type: application/json" \\
-d '{ "tenant_id": "my-team", "query": "Q3 revenue target" }'`}
## Reference
The full endpoint catalog is auto-generated from the FastAPI OpenAPI spec at [/docs/api-reference](/docs/api-reference). The same spec is hosted live at [`/api/openapi.json`](/api/openapi.json) — point any code generator at it.
## Pagination, errors, rate limits
See [Reference → Errors](/docs/reference/errors) and [Reference → Env vars](/docs/reference/env-vars). Standard patterns: `?cursor=...&limit=...` for pagination, `4xx` problem-detail JSON for errors, `Retry-After` header on 429s.
## Long-lived integrations: per-agent keys
The single tenant-scoped `mc_` flow above is fine for one-off scripts. For anything that ships — trust gating, fleet membership, per-agent keystones, real identity in the dashboard — bind each agent to its own **agent-scoped credential** via [Integrations → Per-agent keys](/docs/integrations/per-agent-keys). Both kinds use the `mc_` prefix on the wire; scope is set at mint time.
# Operational Commands
URL: https://memclaw.net/docs/reference/cli
Description: Common commands for self-hosted MemClaw. Defers to the OSS scripts/ directory for the canonical list.
The OSS bundle ships scripts in [`scripts/`](https://github.com/caura-ai/caura-memclaw/tree/main/scripts). The list below covers the everyday commands; check the directory for additions.
## Database migrations
Migrations are managed with Alembic, configured in `alembic.ini`:
```bash
docker compose run --rm core-api alembic upgrade head
docker compose run --rm core-api alembic revision --autogenerate -m "what changed"
```
## Agent skill installer
The OSS API exposes a one-line installer for the `memclaw` agent skill — see `tests/test_install_skill_endpoint.py` for the supported parameters. URL auto-targets this deployment's host:
## Health checks
```bash
curl http://localhost:8000/api/v1/health
```
`core-api` exposes `/api/v1/health` only — it returns the dependency probe shape (`status`, `storage`, `redis`, `event_bus`). Sibling services (`core-storage-api`, `core-worker`, `core-operations`) expose `/readyz` for readiness probes on Cloud Run, where `/healthz` is intercepted by the GFE.
# Environment Variables
URL: https://memclaw.net/docs/reference/env-vars
Description: The authoritative list lives in the OSS .env.example. Highlights below.
The full, current set of environment variables — with comments — lives in [`.env.example`](https://github.com/caura-ai/caura-memclaw/blob/main/.env.example) on the OSS repo. Read it directly; it's the source of truth and stays in sync with the code.
The headline groups (per `.env.example`):
- **Mode** — `IS_STANDALONE`, `ENVIRONMENT`
- **Database** — `POSTGRES_HOST`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_REQUIRE_SSL`
- **Auth** — `ADMIN_API_KEY`, `MEMCLAW_API_KEY`
- **Embedding provider** — `EMBEDDING_PROVIDER`, `OPENAI_API_KEY`, `OPENAI_EMBEDDING_MODEL` (see comments in `.env.example` for `local` / `fake` providers)
- **LLM enrichment / entity extraction** — `ENTITY_EXTRACTION_PROVIDER` plus the matching provider key (`ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `OPENROUTER_API_KEY`, …)
- **Platform-tier providers** — only relevant for the enterprise managed deployment (Vertex AI, platform embedding model). OSS users leave these empty.
## Auth modes
Per `.env.example`:
- `IS_STANDALONE=true` — single-tenant mode. No auth on most routes.
- `ADMIN_API_KEY=…` — gates `/api/admin/*` routes. Required in production.
- `MEMCLAW_API_KEY=…` — when set, **all** non-admin requests must include it via `X-API-Key`. Useful when exposing the API beyond localhost.
# Errors and Status Codes
URL: https://memclaw.net/docs/reference/errors
Description: The canonical error envelope shared by REST and MCP, plus the HTTP status → code mapping.
Both REST and MCP emit the same error shape. Source: [`core-api/src/core_api/errors.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/errors.py) (added in PR #58).
## Canonical envelope
```json
{
"error": {
"code": "",
"message": "",
"details": { "...": "optional" }
}
}
```
Dispatch on `error.code` — it's the machine-readable signal and is identical across both surfaces.
## REST back-compat
REST responses keep a top-level `detail` field alongside `error` so existing clients reading `response.json()["detail"]` keep working:
```json
{
"detail": "Memory not found",
"error": { "code": "NOT_FOUND", "message": "Memory not found" }
}
```
`detail` is the deprecated mirror. New clients should switch to `error.code`.
For FastAPI request-validation failures (422), `detail` is the original list of validation errors and `error.details.errors` carries the same list aggregated.
## MCP envelope
MCP tools return the same `error` envelope, JSON-serialized, plus `_latency_ms`:
```json
{
"error": {
"code": "INVALID_ARGUMENTS",
"message": "Unknown op 'wat'.",
"details": { "op": "wat", "expected_ops": ["read", "update", "..."] }
},
"_latency_ms": 7
}
```
Pre-PR-#58 MCP tools returned bare `"Error (XXX): ..."` strings — that format is gone.
## HTTP status → canonical code
From `STATUS_TO_CODE` in `errors.py`:
| Status | Code |
| --- | --- |
| 400 | `BAD_REQUEST` |
| 401 | `UNAUTHORIZED` |
| 402 | `PAYMENT_REQUIRED` |
| 403 | `FORBIDDEN` |
| 404 | `NOT_FOUND` |
| 405 | `METHOD_NOT_ALLOWED` |
| 408 | `REQUEST_TIMEOUT` |
| 409 | `CONFLICT` |
| 410 | `GONE` |
| 413 | `PAYLOAD_TOO_LARGE` |
| 415 | `UNSUPPORTED_MEDIA_TYPE` |
| 422 | `INVALID_ARGUMENTS` |
| 429 | `RATE_LIMITED` |
| 500 | `INTERNAL_ERROR` |
| 501 | `NOT_IMPLEMENTED` |
| 502 | `UPSTREAM_ERROR` |
| 503 | `UNAVAILABLE` |
| 504 | `UPSTREAM_TIMEOUT` |
Statuses not in the table fall back to `HTTP_` (e.g. `HTTP_418`).
## Trust-level errors
Trust failures come from `core_api.services.trust_service.parse_trust_error` and are re-wrapped as canonical `FORBIDDEN` with the required vs. caller's level in `details`.
## Idempotency
The write route (`POST /api/v1/memories`) accepts an `Idempotency-Key` header (`IDEMPOTENCY_HEADER` in `core-api/src/core_api/middleware/idempotency.py`). A retry within the cache window with the same key short-circuits to the original response without consuming a write slot.
# Authoring skills
URL: https://memclaw.net/docs/skill-factory/authoring
Description: Write a skill directly via memclaw_doc — the required fields, the slug and size rules, create vs update with hash-binding, and how a write is validated and staged.
A skill is a document in the `skills` collection. Any agent or admin can author
one with `memclaw_doc op=write collection='skills'`. The [Skills
page](/docs/skills) shows the bare mechanics; this page is the **Skill
Factory** view — what the lifecycle validator requires, and what happens to
your write when the feature is enabled.
With Skill Factory enabled, a regular agent's write lands as `staged`
(pending [Inbox](/docs/skill-factory/skills-inbox) review), not instantly
`active`. See [Lifecycle & governance](/docs/skill-factory/lifecycle) for the
status model and RBAC.
## A minimal write
```python
await session.call_tool("memclaw_doc", {
"op": "write",
"collection": "skills",
"doc_id": "rotate-staging-db-credentials", # = the slug
"data": {
"name": "Rotate staging DB credentials",
"slug": "rotate-staging-db-credentials",
"description": "Rotate the staging Postgres password and refresh the k8s secret.",
"summary": (
"Use when staging DB auth fails or on the 90-day rotation: "
"generate a new password, update the secret, restart the pods."
),
"domain": "ops",
"kind": "create",
"source": "agent",
"content": "## Steps\n1. ...\n2. ...\n",
},
})
```
## Required fields
The validator rejects (`422`) a write missing any of these top-level `data`
keys:
| Field | Notes |
| --- | --- |
| `name` | Human-readable title. |
| `slug` | Must equal the `doc_id` and match `^[a-z0-9][a-z0-9._-]{0,99}$`. |
| `description` | Short label. Capped at **160 bytes** by default (`skills_factory.description_max_bytes`). |
| `domain` | Free-form grouping (e.g. `ops`, `support`). |
| `kind` | `create` or `update` (see below). |
| `source` | One of `agent`, `manual`, `forge`, `imported`. **Must be present**; the server **enforces** a role-appropriate value (it does not default it): a regular caller must use `agent`; `manual` requires admin; `forge` is reserved for the [Forge](/docs/skill-factory/forge) worker; `imported` is an internal back-compat/migration value. A disallowed value is rejected (`403`). |
| `content` | The skill body (Markdown). Capped at **40,000 bytes**. Required for every normal write; only the internal `source='imported'` back-compat path may omit it. |
`content` (the skill body, Markdown, capped at 40,000 bytes) is **required** for
every normal write. The sole exception is the internal `source='imported'`
back-compat path (skills migrated without a body) — you won't hit it on a
regular `op=write`.
### `description` vs `summary`
Both matter, for different reasons:
- **`description`** is required and length-capped — a tight label.
- **`summary`** is the field that gets **embedded** for semantic search. On the
`skills` collection, search ranks on `summary` (falling back to
`description` if no summary is present). Write a *trigger-shaped* summary —
"Use when …" — not a restatement of the title, or peers won't find it via
`memclaw_doc op=search`.
## Create vs update (hash-binding)
`kind` distinguishes a brand-new skill from a revision of an existing one:
- **`create`** — a new skill at a fresh slug.
- **`update`** — a revision of a live skill. The write must carry
`target.target_content_hash` equal to the current skill's `content_hash`. If
the live skill changed since you read it, the hashes won't match and the
write is rejected — so you re-base your edit on current content rather than
silently clobbering a newer version. (A skill whose target drifts is marked
`stale`.)
An `update` carries the binding under `data.target.target_content_hash`:
```python
await session.call_tool("memclaw_doc", {
"op": "write",
"collection": "skills",
"doc_id": "rotate-staging-db-credentials",
"data": {
"name": "Rotate staging DB credentials",
"slug": "rotate-staging-db-credentials",
"description": "...",
"domain": "ops",
"kind": "update",
"source": "agent",
"content": "## Steps\n1. ...\n",
"target": {
# the current skill's content_hash (read it first)
"target_content_hash": "sha256:…",
},
},
})
```
If `target_content_hash` doesn't match the live skill, the write is rejected
with `409`; if no skill exists at that slug, `404`.
`op=write` is upsert by `(collection, doc_id)`: re-writing the same slug with a
changed `summary` refreshes the embedding; same `summary` with different `data`
refreshes stored fields without re-embedding.
## What the validator does to your write
On a `collection='skills'` write, the server (in order):
1. Checks the required fields are present and typed, the slug is valid, and the
sizes are within caps — else `422`.
2. **Enforces `source`** by role (a regular caller must use `agent`; `manual`
is admin-only; `forge` is Forge-only — else `403`) and **defaults `status`**
by role: a regular caller's write becomes `staged`; only an admin may set
`active`; `candidate` is Forge-only.
3. Runs a synchronous **Sentinel** scan. A `critical` finding rejects the write
(`422`); otherwise the scan state rides on the doc (`clean`) and feeds the
promotion gate.
4. Stamps `content_hash`, `origin.agent_id`, and timestamps.
So a well-formed agent write becomes a `staged`, scanned skill awaiting
[Inbox](/docs/skill-factory/skills-inbox) review — not an immediately live one.
## Best practices
- **Lead the `summary` with the trigger** ("Use when …"). It's the primary
field search ranks on — description is only a fallback.
- **Keep `content` a runbook**, not prose — numbered steps an agent can follow.
- **Revise with `kind='update'`**, not a new slug, so history and hash-binding
stay intact.
- **Expect `staged`.** Don't build flows that assume an agent write is instantly
live; route approval through the Inbox. (`auto_promote_clean` bypasses the
Inbox only for fully-gated Forge candidates — not for agent writes.)
## Where to look in the source
- Write validator, required fields, kinds, hash-binding: [`core-api/src/core_api/services/skill_lifecycle.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/skill_lifecycle.py)
- The `skills` collection / `memclaw_doc` surface: see [Skills](/docs/skills).
# Delivery
URL: https://memclaw.net/docs/skill-factory/delivery
Description: How an active skill actually reaches an agent — the MCP pull tier, the OpenClaw push tier, the active-only gate on both, and how the bundled usage skill differs.
Authoring and approving a skill makes it `active`. **Delivery** is the separate
question of how that active skill gets in front of a running agent. There are
two tiers, with very different reliability — and both serve **active-only** once
Skill Factory is enabled.
## Tier 1 — MCP pull
The agent fetches skills itself through the `memclaw_doc` tool:
- `op=search collection='skills'` — semantic search over skill `summary`
embeddings.
- `op=query collection='skills'` — structured (filter-based) retrieval over the
collection, no embedding needed.
- `op=read collection='skills' doc_id=` — fetch a specific skill.
When `skills_factory.enabled` is on, the server enforces active-only on this
path, with no way for the caller to widen it:
- `read` of a non-active skill → **404** (no existence leak).
- `query` / `search` → `status='active'` is **forced**; a caller-supplied
`status` is rejected (`422`).
- Filtering respects tenant ownership (no cross-tenant bleed).
This tier is **probabilistic**: the agent has to decide to look, and search has
to surface the right skill. Good for discovery; not a guarantee the agent will
use a given skill.
## Tier 2 — OpenClaw push
The [OpenClaw plugin](/docs/integrations/openclaw) reconciler removes the
discovery step. On each heartbeat it calls:
```
POST /api/v1/skills/installable { tenant_id, fleet_id?, limit }
```
and writes every returned skill to the node's managed skills directory as a
`SKILL.md`, then (optionally) registers that directory on OpenClaw's skill load
path. The agent sees the skill in its skill list without searching for it —
**reliable** delivery. Details and configuration in [OpenClaw
plugin](/docs/skill-factory/openclaw-plugin).
The endpoint is deliberately narrow and fail-safe:
- The collection is fixed to `skills` and the filter is **server-decided** — a
harness cannot ask for non-active skills.
- Opted-in tenant → `status='active'` only. Not opted in → every visible skill
(byte-identical to the legacy reconcile — the merge-day no-op).
- If the opt-in flag can't be read, the endpoint returns **503** rather than
fall back to returning everything — an outage can never push a non-active
skill.
Both tiers deliver promptly to the node, but an agent **session already
running** keeps its cached skill list until a fresh session starts. A
newly-active skill reaches new sessions immediately and existing ones on
their next restart.
## Which tier do I get?
| | MCP pull | OpenClaw push |
| --- | --- | --- |
| Who fetches | the agent, on demand | the plugin reconciler, every heartbeat |
| Reliability | probabilistic (must search + choose) | reliable (on disk + in skill list) |
| Requires | any MCP client | the OpenClaw plugin installed on the node |
| Active-only gate | `op=read/query/search` filter | `/skills/installable` server filter |
If your agents run under OpenClaw, the push tier is the dependable path; the
pull tier is always available to any MCP client as a discovery surface.
## Not the same thing: the bundled usage skill
Don't confuse Skill Factory delivery with the **bundled usage skill** installer:
- `GET /api/v1/install-skill` returns a one-liner that installs MemClaw's *own*
usage guide (the `memclaw` skill, or `company-brain`) into Claude Code / Codex.
- `GET /api/v1/skill/{name}` serves those allowlisted static skills.
Those teach an agent how to *use MemClaw*; they are not catalog skills flowing
through the Skill Factory lifecycle. See the OSS README "Install the skill"
section for that installer.
## Where to look in the source
- Push endpoint (`/skills/installable`) + active-only gate: [`core-api/src/core_api/routes/documents.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/documents.py)
- MCP pull filter (`memclaw_doc` over `skills`): [`core-api/src/core_api/mcp_server.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/mcp_server.py)
- Reconciler (push): [`plugin/src/reconcile-skills.ts`](https://github.com/caura-ai/caura-memclaw/blob/main/plugin/src/reconcile-skills.ts)
# Forge
URL: https://memclaw.net/docs/skill-factory/forge
Description: The server-side resident that mines fleet behavior into skill candidates — how it runs, how operators trigger and observe it, and the knobs that tune it.
**Forge** is the half of Skill Factory that authors skills *for* you. Where
[direct authoring](/docs/skill-factory/authoring) needs an agent to remember to
write a skill, Forge watches what the fleet actually does — clusters of
repeated, successful procedures across sessions and agents — and **distills
each cluster into a skill candidate** automatically.
## What Forge produces
Every candidate Forge writes lands with `status='candidate'` and
`source='forge'`. Candidates are **never visible to agents** — they're an
internal staging area. A candidate only becomes useful by clearing the [six
auto-gates](/docs/skill-factory/lifecycle#the-six-auto-gates) and being promoted
to `staged` (then `active` via the [Inbox](/docs/skill-factory/skills-inbox) or
`auto_promote_clean`).
Re-running Forge over the same cluster is safe: a new `candidate` may overwrite
an existing `candidate` at the same slug (that's how Forge refines its own
work), but Forge **will not** overwrite a slug that has already moved past
`candidate` (`staged`, `active`, `rejected`, `quarantined`, `stale`, or
`deprecated`) — it skips it and records `candidates_skipped_existing`.
## How it runs (operator setup)
Forge runs per tenant on a cadence **you** drive, via a fan-out endpoint:
```
external scheduler ──POST /admin/lifecycle/fanout/forge-distill──▶ core-api
│
lists tenants with skills_factory.enabled=true
│
publishes memclaw.lifecycle.forge-distill-requested │ (per tenant)
▼
run_forge_cron_tick(tenant)
```
You point a scheduler (Google Cloud Scheduler, a Kubernetes `CronJob`, cron +
`curl`, …) at the endpoint:
```bash
curl -fsS -X POST "$CORE_API_BASE_URL/admin/lifecycle/fanout/forge-distill"
```
The cadence is entirely the scheduler's. `skills_factory.forge.cron_interval_hours`
(default `6`) is **informational only** — set it to match your real schedule so
operators reading the config aren't misled. A 1-hour dedup window means an
extra fan-out inside that window is a no-op for a tenant that already ran.
Forge does nothing for a tenant until `skills_factory.enabled = true`. The
fan-out lists only opted-in tenants, so enabling/disabling the flag is the
on/off switch.
## What operators see
Each tick writes **one `lifecycle_audit` row per tenant**, with per-run counts
under `stats`:
- `candidates_written` — new candidates persisted.
- `candidates_skipped_poisoned` — cluster fingerprint is in the reject poison
table (an operator rejected a prior version).
- `candidates_skipped_sentinel` — the in-Forge scan flagged the draft.
- `candidates_skipped_existing` — the slug already moved past `candidate`.
- `candidates_skipped_distill_error` / `candidates_skipped_io_error` —
distillation or persistence failures (operator-actionable).
Fresh candidates that pass the gates appear in the [Skills
Inbox](/docs/skill-factory/skills-inbox). A failure in one tenant's tick never
aborts another tenant's.
## Tuning
| Key (`skills_factory.*`) | Default | Effect |
| --- | --- | --- |
| `forge.cron_interval_hours` | `6` | Informational; your scheduler sets the real cadence. |
| `forge.min_cluster_size` | `3` | Minimum executions in a cluster (the volume gate). |
| `forge.min_distinct_agents` | `3` | Minimum distinct agents in a cluster (the diversity gate). |
| `forge.freshness_window_days` | `14` | How recent a cluster's activity must be (the freshness gate). |
| `sentinel.auto_promote_clean` | `false` | If true, a candidate that passes all gates **and** scans clean skips the Inbox and goes straight to `active`. A trust decision — see below. |
### Auto-promote: shipping without a human
`sentinel.auto_promote_clean=true` removes the human review step for clean,
fully-gated candidates. Only enable it once you trust that the gate thresholds
and the Sentinel rule-set are tuned for your content — otherwise keep it
`false` and approve from the Inbox.
## Where to look in the source
- Forge orchestrator + run result: [`core-api/src/core_api/services/forge/forge_service.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/forge/forge_service.py)
- Cron tick + fan-out consumer: [`core-api/src/core_api/services/forge/cron_handler.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/forge/cron_handler.py)
- Operator runbook (OSS): [`docs/operator-forge-cron.md`](https://github.com/caura-ai/caura-memclaw/blob/main/docs/operator-forge-cron.md)
# Skill Factory
URL: https://memclaw.net/docs/skill-factory
Description: How MemClaw turns proven fleet behavior into governed, delivered SKILL.md skills — authored by agents or distilled by Forge, gated through a lifecycle, and pushed to the harnesses that run your agents.
The [Skills page](/docs/skills) covers the simplest case: an agent writes a
`SKILL.md`-style document into the `skills` collection and peers find it via
search. **Skill Factory** is the system around that — it answers three
questions the bare collection doesn't:
1. **Where do good skills come from?** Agents author them directly, *and*
**Forge** distills them automatically from what the fleet has repeatedly
done well.
2. **How do we trust them?** Every skill moves through a governed lifecycle —
`candidate → staged → active` — gated by automated checks, a security scan,
and (optionally) human review.
3. **How do they actually reach an agent?** Two delivery tiers: agents *pull*
active skills over MCP, or the OpenClaw plugin *pushes* them onto each
node's skill load path.
Skill Factory is **opt-in per tenant** and **off by default**. With it
disabled, the `skills` collection behaves exactly as the [Skills
page](/docs/skills) describes — no lifecycle, no gating, every stored skill
visible. Turning it on activates the three pillars below.
Enable it by setting `skills_factory.enabled = true` in the tenant's org
settings. While it's `false`, none of the gating, Forge, or Inbox behavior
runs — the feature is a true no-op until you opt in.
## The three pillars
### 1. Authoring — agents *and* Forge
A skill can enter the catalog two ways:
- **Direct authorship.** An agent (or an admin) writes a skill with
`memclaw_doc op=write collection='skills'`, exactly as on the
[Skills page](/docs/skills). When Skill Factory is enabled, that write is
validated and lands as `staged` (pending review) rather than instantly
visible — see [Lifecycle & governance](/docs/skill-factory/lifecycle).
- **Forge.** A server-side resident that mines the fleet's memory and outcome
signals, clusters repeated successful procedures, and **distills them into
skill candidates** — no agent has to remember to write the skill. Forge runs
as a low-trust resident (it can only produce team-scoped skills) on a cadence
you control.
### 2. Governance — the lifecycle
Every skill carries a `status`. The states form a one-way street toward
`active`, with side exits for anything that fails a check:
```
┌─────────────┐
Forge ─────▶ │ candidate │ (internal — never agent-visible)
└──────┬──────┘
auto-gates │ pass
▼
agent write ───▶┌─────────────┐ approve (HITL) ┌──────────┐
│ staged │ ─────────────────▶ │ active │ ◀── admin direct-write
└─────┬───────┘ └──────────┘
│ reject / scan-flag / hash-drift
▼
rejected · quarantined · stale · deprecated
```
Automated **auto-gates** and a **Sentinel** security scan decide what may be
promoted; a **Skills Inbox** lets an operator approve, edit, reject, or
quarantine staged skills. Only `active` skills are ever delivered to agents.
Full detail in [Lifecycle & governance](/docs/skill-factory/lifecycle).
### 3. Delivery — pull and push
Getting a skill to an agent has a hard ceiling depending on the tier:
| Tier | Mechanism | Reliability |
| --- | --- | --- |
| **MCP pull** | Agent calls `memclaw_doc op=search` / `op=read` on the `skills` collection and decides to use what it finds. | Probabilistic — the agent has to look, and search has to surface it. |
| **Harness push** | The OpenClaw plugin's reconciler pulls every `active` skill from `POST /api/v1/skills/installable` and writes each one to the node's skill directory, then registers that directory on OpenClaw's load path. | Reliable — the skill is on disk and in the agent's skill list, no discovery step. |
Both tiers serve **only `active` skills** once Skill Factory is enabled — the
pull path filters server-side, and the push endpoint returns active-only. A
Forge candidate sitting in the Inbox reaches no one until it's approved.
Newly-active skills land on a node's disk and skill registry promptly, but an
agent **session that is already running** keeps its cached skill list until a
fresh session starts. Plan rollouts accordingly.
## Where to go next
- **[Lifecycle & governance](/docs/skill-factory/lifecycle)** — the statuses,
the six auto-gates, the Sentinel scan, and the Skills Inbox review flow.
- **[Skills](/docs/skills)** — authoring and discovering skills directly via
`memclaw_doc` (the foundation Skill Factory builds on).
- **[OpenClaw integration](/docs/integrations/openclaw)** — installing the
plugin that performs harness-push delivery.
## Where to look in the source
- Lifecycle validator + auto-gates: [`core-api/src/core_api/services/skill_lifecycle.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/skill_lifecycle.py)
- Forge resident: [`core-api/src/core_api/services/forge/`](https://github.com/caura-ai/caura-memclaw/tree/main/core-api/src/core_api/services/forge)
- Skills Inbox API: [`core-api/src/core_api/routes/skills_inbox.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/skills_inbox.py)
- Harness-push delivery (plugin reconciler): [`plugin/src/reconcile-skills.ts`](https://github.com/caura-ai/caura-memclaw/blob/main/plugin/src/reconcile-skills.ts)
# Lifecycle & governance
URL: https://memclaw.net/docs/skill-factory/lifecycle
Description: The skill status model, who can set each status, the six auto-promotion gates, the Sentinel security scan, and the Skills Inbox review flow.
Every document in the `skills` collection carries a `status`. When Skill
Factory is enabled, that status governs both **who can write it** and **whether
agents can see it** — only `active` skills are ever delivered. This page is the
reference for how a skill moves through those states.
All of the behavior below is gated by `skills_factory.enabled` in the
tenant's org settings. With it `false`, writes are not status-defaulted, no
gates run, and every stored skill is visible — see the
[overview](/docs/skill-factory).
## Statuses
| Status | Meaning | Who can set it |
| --- | --- | --- |
| `candidate` | Freshly minted by Forge; **never visible to agents**. Awaiting auto-gate evaluation. | Forge only (internal) |
| `staged` | Passed the gates (or written directly by an agent); **pending review** in the Skills Inbox. | Default for any agent/admin `op=write` |
| `active` | Approved and **delivered to agents** (pull and push). | Admin direct-write, or Inbox **approve** |
| `rejected` | An operator declined it; its cluster fingerprint is poison-flagged for a cool-off window. | System (Inbox reject) |
| `quarantined` | The Sentinel scan flagged a critical finding; held out of delivery. | System (Sentinel / Inbox) |
| `stale` | The skill it was meant to update changed underneath it (content-hash drift); must be re-revised. | System (lifecycle) |
| `deprecated` | Superseded or retired. | System (lifecycle) |
The write path enforces this with three rules
([`skill_lifecycle.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/skill_lifecycle.py)):
- A regular agent `op=write` **always lands as `staged`** — an agent cannot
self-publish an `active` skill.
- Only an **admin** may write `active` directly.
- `candidate` is **Forge-only**, and `rejected` / `quarantined` / `stale` /
`deprecated` are **system-only** — an agent or operator cannot hand-set them
on a write, so a skill can't be silently retired or hidden from review.
## Authoring writes: validation
A `op=write collection='skills'` is validated and normalized before it lands:
- **Required fields** and a filesystem-safe slug (`^[a-z0-9][a-z0-9._-]{0,99}$`).
- **`description` length cap** — default **160 bytes**, configurable via
`skills_factory.description_max_bytes`.
- **Body cap** — 40,000 bytes.
- **Sentinel pre-scan** (below) runs synchronously; a critical finding rejects
the write.
- **Hash-binding** — an update (`kind='update'`) must carry the
`target_content_hash` of the live skill it revises; if that skill has since
changed, the write is rejected so you re-base on current content.
## The six auto-gates
Forge candidates don't reach the Inbox until they clear **all six** gates
(re-checked at promotion time, because the world moves between Forge's write
and the lifecycle worker's run). The evaluator **fails closed** — a gate it
can't confidently evaluate (missing inputs) counts as a failure, so an
unverifiable candidate is held rather than promoted.
| Gate | Check | Default threshold |
| --- | --- | --- |
| **G1 · volume** | `origin.cluster_size ≥ min_cluster_size` | `min_cluster_size = 3` |
| **G2 · diversity** | `origin.distinct_agents ≥ min_distinct_agents` | `min_distinct_agents = 3` |
| **G3 · freshness** | cluster window end within N days of now | `freshness_window_days = 14` |
| **G4 · poison** | the cluster fingerprint is **not** in the reject poison table | — |
| **G5 · scan** | `data.scan.state == 'clean'` (no Sentinel quarantine) | — |
| **G6 · hash_binding** | for `kind='update'`, `target.target_content_hash` still matches the live skill | — |
The volume, diversity, and freshness thresholds are tunable per tenant under
`skills_factory.forge.*`. A candidate that passes all six is promoted
`candidate → staged` and appears in the Inbox.
## Sentinel scan
Sentinel is MemClaw's content-safety scan for skills (prompt-injection and PII
patterns). It runs **synchronously on every write** and again before an Inbox
approval:
- `state = 'clean'` and `critical = 0` → the write proceeds (and G5 can pass).
- A **critical** finding → the write is rejected (`422`) on authoring, or the
skill is moved to `quarantined` in the lifecycle.
- Findings are bucketed `critical` / `warn` / `info`; only `critical` blocks.
Sentinel is a gate, not a guarantee — treat `warn`/`info` findings as review
signals in the Inbox, not noise.
## The Skills Inbox (human review)
Staged skills surface in the **Skills Inbox** — the human-in-the-loop review
queue. Its endpoints live under `POST/GET /api/v1/skills-inbox/` and **require
an admin**; every endpoint 4xx's with `SKILLS_FACTORY_DISABLED` if the feature
isn't enabled for the tenant.
| Action | Endpoint | Effect |
| --- | --- | --- |
| **List** | `GET /api/v1/skills-inbox/` | Staged skills for the tenant (filter by `fleet_id`); capped by `skills_factory.inbox_max_pending`. |
| **Approve** | `POST /api/v1/skills-inbox/{slug}/approve` | Re-scans, then `staged → active` — the skill goes live. |
| **Reject** | `POST /api/v1/skills-inbox/{slug}/reject` | `staged` or `quarantined → rejected` and poison-flags the fingerprint for a cool-off (`skills_factory.rejection_cooloff_days`) so Forge won't immediately re-mint it. |
| **Quarantine** | `POST /api/v1/skills-inbox/{slug}/quarantine` | `staged → quarantined` — held for investigation. |
| **Defer** | `POST /api/v1/skills-inbox/{slug}/defer` | Leaves it staged; stamps a `deferred_at` marker to push it down the queue. |
| **Edit** | `POST /api/v1/skills-inbox/{slug}/edit` | Revise `content` / `description` / `summary`; re-hashes and re-scans; stays `staged` for a fresh decision. |
### Skipping human review
For tenants that want Forge to ship without a human in the loop, set
`skills_factory.sentinel.auto_promote_clean = true`. A candidate that passes
all six gates **and** scans clean is then promoted straight to `active`,
bypassing the Inbox. It defaults to `false` — review-first.
## Configuration reference
All keys live under `skills_factory` in the tenant's org settings:
| Key | Default | Purpose |
| --- | --- | --- |
| `enabled` | `false` | Master opt-in. Off = legacy no-op (no gating, no Forge, no Inbox). |
| `description_max_bytes` | `160` | Cap on a skill's `description`. |
| `inbox_max_pending` | — | Cap on the number of staged skills the Inbox lists. |
| `rejection_cooloff_days` | — | Poison-table cool-off after a reject. |
| `forge.cron_interval_hours` | `6` | Informational; your external scheduler must match (see OpenClaw / operator setup). |
| `forge.min_cluster_size` | `3` | G1 volume threshold. |
| `forge.min_distinct_agents` | `3` | G2 diversity threshold. |
| `forge.freshness_window_days` | `14` | G3 freshness threshold (days). |
| `sentinel.auto_promote_clean` | `false` | Skip the Inbox for clean, fully-gated candidates. |
## Where to look in the source
- Status model, RBAC, write validation, auto-gates: [`core-api/src/core_api/services/skill_lifecycle.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/skill_lifecycle.py)
- Skills Inbox API: [`core-api/src/core_api/routes/skills_inbox.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/skills_inbox.py)
- Forge resident + Sentinel scan: [`core-api/src/core_api/services/forge/`](https://github.com/caura-ai/caura-memclaw/tree/main/core-api/src/core_api/services/forge)
# OpenClaw plugin delivery
URL: https://memclaw.net/docs/skill-factory/openclaw-plugin
Description: How the OpenClaw plugin's reconciler pushes active skills onto a node — managed vs shared target dirs, the ownership marker, registering on the load path, and per-node observability.
The OpenClaw plugin is the [push delivery tier](/docs/skill-factory/delivery).
On each heartbeat its **reconciler** converges a node's on-disk skills with the
tenant's active catalog — so an approved skill reliably lands on the node and
shows up in the agent's skill list, no search required. This page covers how it
behaves and how to configure it.
## The reconcile loop
Each heartbeat the reconciler:
1. Pulls the active catalog from `POST /api/v1/skills/installable` (active-only
when the tenant has opted in — see [Delivery](/docs/skill-factory/delivery)).
2. Writes each skill to a managed skills directory as `/SKILL.md`.
3. Prunes skills it manages that are no longer in the catalog.
4. Reports the outcome on the heartbeat for observability (below).
The bundled `memclaw` usage skill is **protected** — it's never pruned.
## Target directories
By default the reconciler manages exactly one directory — the plugin's own
`skills/` dir, in **owned** mode. Operators can add more via the
`MEMCLAW_SKILL_TARGETS` environment variable, a JSON array of
`{ dir, mode, register? }`:
```bash
MEMCLAW_SKILL_TARGETS='[
{ "dir": "/srv/shared/skills", "mode": "additive", "register": true }
]'
```
### `owned` vs `additive`
| Mode | MemClaw's authority | Use for |
| --- | --- | --- |
| **`owned`** | Full. Anything on disk **not** in the catalog is pruned (destructive). | Dirs MemClaw exclusively controls. |
| **`additive`** | Minimal. MemClaw only ever touches entries **it wrote**. | A shared/foreign dir that also holds the client's own skills. |
`additive` mode is fail-safe by design, tracked with a per-skill
`.memclaw-owned` marker file:
- A slug already occupied by an **unowned** skill is a **collision** — skipped,
never overwritten (reported separately from catalog-shape errors).
- An unowned skill is **never removed**, even when the catalog is empty.
- Only marker-bearing entries are updated or pruned.
This confines the "empty catalog / wrong tenant wipes the directory" hazard to
`owned` dirs only — an `additive` dir can lose *MemClaw's own* entries but never
the client's.
### `register`: reaching agents
Writing a skill to disk isn't enough if OpenClaw doesn't scan that directory.
The plugin's own `owned` dir is published as a plugin skill automatically; an
**extra** dir is not. Set `register: true` and the reconciler adds the dir to
`skills.load.extraDirs` in `~/.openclaw/openclaw.json` — OpenClaw's documented,
watched mechanism for extra skill directories. The write is append-only and
idempotent (it never duplicates an entry, preserves existing/foreign entries,
and refuses to clobber a malformed config).
`skills.load.watch` defaults on, so a newly-registered dir is picked up
without a restart — but an agent **session already running** keeps its cached
skill list until a fresh session (`--session-key`) starts.
## Observability on the heartbeat
The reconciler's summary rides the heartbeat and is stored as the latest
snapshot per node, surfaced via `GET /api/v1/fleet/nodes`. It carries:
- `installed` — active skills currently on the node (standing truth, every
tick).
- `added` / `removed` — this tick's deltas.
- `skipped` — catalog rows that were malformed.
- `collisions` — additive-dir slugs skipped because an unowned skill held the
slot (kept distinct from `skipped`).
- `protected` — catalog-absent skills deliberately kept (e.g. `memclaw`).
- `targets[]` — the same fields broken down **per target dir**, so an operator
can see *which* dir a skill landed in or collided in.
- `registeredDirs` — the dirs MemClaw has ensured are on `skills.load.extraDirs`.
So an operator who flips a skill to `active` can confirm on `/fleet/nodes` that
it actually reached each node — closing the loop from "approved" to "installed
on the fleet."
## Where to look in the source
- Reconciler, target modes, ownership marker, extraDirs registration: [`plugin/src/reconcile-skills.ts`](https://github.com/caura-ai/caura-memclaw/blob/main/plugin/src/reconcile-skills.ts)
- Delivery contract + OSS deep-dive: [`docs/mcp-skill-delivery.md`](https://github.com/caura-ai/caura-memclaw/blob/main/docs/mcp-skill-delivery.md)
- Installing the plugin: [OpenClaw integration](/docs/integrations/openclaw).
# Reference
URL: https://memclaw.net/docs/skill-factory/reference
Description: One-page reference for Skill Factory — config keys, statuses, auto-gates, endpoints, the skill schema, and the plugin target env var.
A consolidated quick-reference. See the linked pages for detail.
## Config — `org_settings.skills_factory.*`
| Key | Default | Purpose |
| --- | --- | --- |
| `enabled` | `false` | Master opt-in. Off = legacy no-op (no gating, Forge, or Inbox). |
| `description_max_bytes` | `160` | Cap on a skill's `description`. |
| `inbox_max_pending` | — | Cap on staged skills the [Inbox](/docs/skill-factory/skills-inbox) lists. |
| `rejection_cooloff_days` | — | Poison-table cool-off after a reject. |
| `forge.cron_interval_hours` | `6` | Informational; your scheduler sets the real cadence. |
| `forge.min_cluster_size` | `3` | Volume gate threshold. |
| `forge.min_distinct_agents` | `3` | Diversity gate threshold. |
| `forge.freshness_window_days` | `14` | Freshness gate threshold (days). |
| `sentinel.auto_promote_clean` | `false` | Skip the Inbox for clean, fully-gated candidates. |
Enable: `PATCH org_settings { "skills_factory": { "enabled": true } }`.
## Statuses
`candidate` (Forge-only, never agent-visible) → `staged` (default for writes;
in the Inbox) → `active` (delivered). Side states: `rejected`, `quarantined`,
`stale`, `deprecated` (all system-set). Full RBAC in
[Lifecycle](/docs/skill-factory/lifecycle#statuses).
## The six auto-gates
`candidate → staged` requires **all six** (fail-closed):
| Gate | Check | Default |
| --- | --- | --- |
| volume | `origin.cluster_size ≥ min_cluster_size` | `3` |
| diversity | `origin.distinct_agents ≥ min_distinct_agents` | `3` |
| freshness | cluster window within `forge.freshness_window_days` | `14` |
| poison | fingerprint not in the reject poison table | — |
| scan | `data.scan.state == 'clean'` | — |
| hash_binding | for `kind='update'`, target hash still matches | — |
## Endpoints
| Method · Path | Purpose | Page |
| --- | --- | --- |
| `POST /api/v1/skills/installable` | Active-only catalog for harness push | [Delivery](/docs/skill-factory/delivery) |
| `GET /api/v1/skills-inbox/` | List staged skills | [Inbox](/docs/skill-factory/skills-inbox) |
| `POST /api/v1/skills-inbox/{slug}/approve` | `staged → active` | Inbox |
| `POST /api/v1/skills-inbox/{slug}/edit` | Revise + re-scan; stays staged | Inbox |
| `POST /api/v1/skills-inbox/{slug}/reject` | `staged` or `quarantined` → `rejected` + cool-off | Inbox |
| `POST /api/v1/skills-inbox/{slug}/quarantine` | `staged → quarantined` | Inbox |
| `POST /api/v1/skills-inbox/{slug}/defer` | Stay staged; push down queue | Inbox |
| `POST /admin/lifecycle/fanout/forge-distill` | Trigger a Forge tick per opted-in tenant | [Forge](/docs/skill-factory/forge) |
(`memclaw_doc op=search/query/read collection='skills'` is the [MCP pull](/docs/skill-factory/delivery) surface.)
## Skill schema (`memclaw_doc op=write collection='skills'`)
Required `data` keys: `name`, `slug`, `description`, `domain`, `kind`, `source`.
`content` is required for every normal write; the only exception is the
internal `source='imported'` back-compat path.
- `source` ∈ `{ agent, manual, forge, imported }` — required and **role-enforced**
(a regular caller must use `agent`; `manual` is admin-only; `forge` is
worker-only; `imported` is migration-only — see [Authoring](/docs/skill-factory/authoring)).
- `slug` matches `^[a-z0-9][a-z0-9._-]{0,99}$` and equals the `doc_id`.
- `kind` ∈ `{ create, update }`; an `update` carries
`target.target_content_hash`.
- `description` ≤ `description_max_bytes` (default 160); `content` ≤ 40,000 bytes.
- `summary` is the embedded (searchable) field — write a trigger-shaped sentence.
Detail: [Authoring](/docs/skill-factory/authoring).
## Plugin target env var
`MEMCLAW_SKILL_TARGETS` — JSON array of `{ dir, mode, register? }`:
- `mode`: `owned` (destructive prune) | `additive` (only touches `.memclaw-owned`
entries).
- `register: true` → adds `dir` to `skills.load.extraDirs` in `openclaw.json`.
Detail: [OpenClaw plugin](/docs/skill-factory/openclaw-plugin).
## Where to look in the source
- Lifecycle, gates, schema validation: [`skill_lifecycle.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/skill_lifecycle.py)
- Inbox: [`skills_inbox.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/skills_inbox.py)
- Forge: [`services/forge/`](https://github.com/caura-ai/caura-memclaw/tree/main/core-api/src/core_api/services/forge)
- Push delivery + reconciler: [`documents.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/documents.py), [`reconcile-skills.ts`](https://github.com/caura-ai/caura-memclaw/blob/main/plugin/src/reconcile-skills.ts)
# Skills Inbox
URL: https://memclaw.net/docs/skill-factory/skills-inbox
Description: The human-in-the-loop review queue for staged skills — the card an operator sees, the five actions, and how to recover a quarantined skill.
The **Skills Inbox** is where a `staged` skill waits for a human decision
before it goes live. Both Forge candidates (after they clear the
[gates](/docs/skill-factory/lifecycle#the-six-auto-gates)) and direct
[agent writes](/docs/skill-factory/authoring) land here. Until someone acts,
the skill is **not delivered** to any agent.
The Inbox is an **admin** surface under `POST/GET /api/v1/skills-inbox/`.
Authenticate with an admin-scoped API key — see [Authentication](/docs/authentication).
Every endpoint returns `SKILLS_FACTORY_DISABLED` if the tenant hasn't opted in.
If you've set `skills_factory.sentinel.auto_promote_clean = true`, clean
fully-gated **Forge candidates** bypass the Inbox and go straight to `active`.
Agent writes always land in the Inbox regardless of that setting — the Inbox
then holds candidates that need attention (warnings, direct agent writes, edits).
## The card
`GET /api/v1/skills-inbox/` returns the staged skills for a tenant (filter by
`fleet_id`; capped by `skills_factory.inbox_max_pending`). Each item is an
**InboxCard** — enough to make a decision without leaving the queue:
| Field | What it tells you |
| --- | --- |
| `name`, `description`, `summary` | What the skill is and when it should fire. |
| `domain`, `tags`, `kind` | Grouping; `create` vs `update`. |
| `source` | `forge` (auto-mined), `agent`, or `manual`. |
| `scan_state`, `scan_critical`, `scan_warn` | Sentinel result — `clean` with zero criticals is required to approve. |
| `origin`, `evidence` | For Forge cards: the cluster (size, distinct agents, window) and the traces behind it. |
| `fingerprint` | The cluster fingerprint (drives poison/dedup). |
| `content_hash`, `created_at`, `deferred_at` | Provenance + queue position. |
## The five actions
| Action | Endpoint | Body | Effect |
| --- | --- | --- | --- |
| **Approve** | `POST /{slug}/approve` | — | Re-scans, then `staged → active` **only if the scan is clean** — the skill goes live (pull + push). A non-clean re-scan is **refused (`422`)** and the skill stays `staged`; fix it with **Edit**. |
| **Edit** | `POST /{slug}/edit` | `{ content?, description?, summary? }` | Revise in place; re-hashes and **re-scans**; stays `staged` for a fresh decision. |
| **Reject** | `POST /{slug}/reject` | `{ reason, cooloff_days? }` | `staged` or `quarantined` → `rejected`; poison-flags the fingerprint for a cool-off (defaults to `skills_factory.rejection_cooloff_days`) so Forge won't immediately re-mint it. |
| **Quarantine** | `POST /{slug}/quarantine` | `{ reason }` | `staged → quarantined`; held out of delivery for investigation. |
| **Defer** | `POST /{slug}/defer` | `{ reason? }` | Leaves it `staged`; stamps `deferred_at` to push it down the queue. Deferred skills have no TTL — they stay staged indefinitely until an operator acts. |
Approve/reject/quarantine return an `ActionResponse` with `previous_status`
and the new status, so the transition is auditable.
## Typical workflow
1. **Triage the queue.** Sort by `scan_state` — anything not `clean` needs a
closer look. For Forge cards, check `origin`/`evidence` to see how strong the
cluster is.
2. **Approve** the good ones — they go live immediately.
3. **Edit** a promising-but-rough skill (tighten the `summary`, fix the
`content`); it re-scans and stays staged so you can approve the revision.
4. **Reject** noise with a `reason` — the cool-off stops Forge re-proposing the
same cluster right away.
5. **Defer** anything you're unsure about to revisit later.
## Recovering a quarantined skill
A `quarantined` skill (Sentinel flagged a `critical`, or you quarantined it
manually) is held out of delivery. **Approve** and **Edit** operate only on
`staged` skills — you cannot fix a quarantined skill in place or force it live
past a critical scan. The only Inbox action available on a quarantined skill is
**Reject** (which also poison-flags the cluster fingerprint for a cool-off). To
ship a corrected version, publish a fixed skill afresh, or let
[Forge](/docs/skill-factory/forge) re-propose a clean candidate once the
cool-off lapses. (Direct `quarantined → staged` recovery is planned lifecycle
work.)
## Where to look in the source
- Inbox endpoints, card model, action request bodies: [`core-api/src/core_api/routes/skills_inbox.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/routes/skills_inbox.py)
- Status transitions: [`core-api/src/core_api/services/skill_lifecycle.py`](https://github.com/caura-ai/caura-memclaw/blob/main/core-api/src/core_api/services/skill_lifecycle.py)
# Skills
URL: https://memclaw.net/docs/skills
Description: Publish a proven workflow to the tenant skills catalog so other agents discover it via semantic search.
A **skill** is a reusable, named procedure — a small runbook the rest of the fleet can find, follow, and adapt. MemClaw stores skills as documents in the system-reserved `skills` collection. Any agent in the tenant can author a skill via `memclaw_doc op=write collection='skills'`; any agent can retrieve them via `memclaw_doc op=search`.
If you want to **read** the canonical agent-facing skill an LLM follows when it talks to MemClaw itself, that lives at [`/docs/agents`](/docs/agents). This page is about publishing your **own** skills.
This page covers the bare `skills` collection. **[Skill
Factory](/docs/skill-factory)** is the governed system on top of it — Forge
auto-distills skills from fleet behavior, a `candidate → staged → active`
lifecycle gates what goes live, and a delivery layer pushes active skills
onto your agents' harnesses. When Skill Factory is enabled, the writes
described below land as `staged` (pending review) rather than instantly
visible.
## Authoring: `memclaw_doc op=write collection='skills'`
```python
result = await session.call_tool(
"memclaw_doc",
{
"op": "write",
"collection": "skills",
"doc_id": "triage-cve-rollback-request",
"data": {
"summary": (
"Triage a customer asking to roll back to an older "
"version: check the CVE list, refuse versions with "
"active advisories, suggest the nearest safe build."
),
"steps": [
"Identify the requested target version.",
"Search the kb collection for matching CVE advisories.",
"If a match is found, refuse the rollback and cite the advisory.",
"Otherwise, suggest the nearest patched build.",
],
"owner_agent": "support-admin",
},
},
)
```
The `data` dict is free-form per skill, but two fields are semantically meaningful:
- **`data["summary"]`** (1–3 dense, intent-focused sentences). This is the **only** field that gets embedded. A skill without a `summary` stores fine but won't appear in `memclaw_doc op=search` results — which means peers won't find it.
- **`data["description"]`** — back-compat alias for `summary`; only honored for the skills collection. Prefer `summary`.
Everything else (`steps`, `owner_agent`, your domain fields) is stored as-is and returned verbatim on read.
### `doc_id` slug rules
Skills' `doc_id` becomes a directory name on plugin runtimes that materialise the skill to disk, so the slug is filesystem-safe:
```
^[a-z0-9][a-z0-9._-]{0,99}$
```
Lowercase letters, digits, `.`, `_`, `-`. Must start with a letter or digit. Max 100 chars. Anything else returns `INVALID_ARGUMENTS`.
Good: `triage-cve-rollback-request`, `nightly.report.summarizer`, `slack-thread-cleanup-v2`.
Bad: `Triage CVE!`, `_leading-underscore`, `path/with/slashes`, `caps-and-spaces here`.
## Discovery: `memclaw_doc op=search collection='skills'`
```python
result = await session.call_tool(
"memclaw_doc",
{
"op": "search",
"collection": "skills",
"query": "what to do when a customer asks for an old version",
"top_k": 5,
},
)
```
Search ranks by semantic similarity over the `data["summary"]` vectors, so good summaries are the difference between a skill that gets reused and one that rots. Treat the summary like the bullet you'd write in a runbook index — `what the skill is for`, `when it applies`, `what makes it different from neighbours`.
Omit `collection` to span every collection in the tenant (broad search across kb, skills, your own collections, etc.).
## Reading a skill: `memclaw_doc op=read`
```python
result = await session.call_tool(
"memclaw_doc",
{
"op": "read",
"collection": "skills",
"doc_id": "triage-cve-rollback-request",
},
)
```
Returns the full `data` payload exactly as written.
## Updating
`op=write` is upsert by `(collection, doc_id)`. Re-call with the same `doc_id` and a changed `summary` to refresh the embedding; re-call with the same `summary` but a different `data` to refresh the stored fields without re-embedding.
## Reading MCP tool results
Every `memclaw_doc` call returns an MCP tool result with an `isError` boolean. On gateway-side refusals (slug rules, missing collection, FORBIDDEN, etc.) the server sets `isError=True` and the JSON `{"error": {...}}` envelope lands in `content[0].text`. See [MCP integration → Reading MCP tool results](/docs/integrations/mcp#reading-mcp-tool-results).
## Common pitfalls
- **Skill doesn't show up in search.** You probably wrote without `data["summary"]`. The doc is stored, but there's no embedding to match — search ranks by summary vectors. Re-write with a summary.
- **`INVALID_ARGUMENTS: collection='skills' requires doc_id matching …`.** Your slug has spaces, uppercase, slashes, or starts with `.`, `_`, or `-`. Rename to fit `^[a-z0-9][a-z0-9._-]{0,99}$`.
- **Search across collections returns kb hits instead of skills.** Pass `collection: "skills"` to narrow.
# Build an agent fleet on MemClaw Cloud
URL: https://memclaw.net/docs/tutorials/cloud-fleet
Description: The managed (Cloud) tutorial — a self-contained, end-to-end build. Wire a governed three-agent fleet to memclaw.net: sign up, connect Claude Code, and watch shared memory compound, with just an API key.
**Managed (Cloud) tutorial — self-contained, end-to-end.** MemClaw runs the engine and the Prism dashboard for you; you bring an `mc_` key. Just want to connect a single agent fast? That's the [Quickstart](/docs/getting-started/quickstart). Prefer to run MemClaw yourself? The [self-hosted OSS series](/docs/tutorials/multi-agent-fleet) builds the same fleet on your own Docker stack.
Every AI agent you run today is a brilliant amnesiac.
It debugs a gnarly authentication issue at 2 PM, and by 2:05 — new session, new context window — that hard-won knowledge is gone. Worse: if you run *several* agents, each one re-learns the same lessons in its own silo. Your reviewer agent doesn't know what your dev agent discovered yesterday. Your docs agent contradicts both of them.
The fix is **shared memory** — and once more than one agent can write to it, you immediately need *governance*: who can read what, who can write what, and an audit trail when something goes wrong. That's exactly what [MemClaw](https://memclaw.net) does: an MCP-native memory layer for agent fleets where agents write plain text and MemClaw turns it into enriched, searchable, permissioned memory that improves with use. It's built for fleets running hundreds of agents in production.
**MemClaw Cloud** is the managed platform: a hosted memory engine with 12 MCP tools, built-in governance, and the **Prism** dashboard, fully operated for you. In this tutorial we'll wire up a real **three-agent development fleet** against it, with just an API key:
- **`backend-dev`** — writes code, records decisions and gotchas
- **`code-reviewer`** — reviews PRs, recalls past decisions before nitpicking
- **`docs-writer`** — keeps documentation consistent with what the other two actually did
Our agent harness is **Claude Code** — Anthropic's terminal-based coding agent. It speaks MCP natively, and MemClaw ships a one-line skill installer for it. No frameworks, no orchestration code, no SDKs. Just a key and some config.
---
## The architecture
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Claude Code │ │ Claude Code │ │ Claude Code │
│ backend-dev │ │ code-reviewer│ │ docs-writer │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ MCP over HTTPS · X-API-Key: mc_… │
└──────────────┬───────────┬──────────┘
▼
┌───────────────────┐
│ MemClaw Cloud │
│ Managed engine + │
│ hosted MCP │
│ Prism dashboard │
│ memclaw.net │
└───────────────────┘
one shared, governed brain
```
Each Claude Code instance is an independent agent with its own `agent_id`. They never talk to each other directly. They communicate *through memory* — one writes, the others recall. This is the pattern that scales: the same loop runs unchanged across hundreds of agent identifiers and tens of thousands of memories, at single-digit-millisecond search latency.
---
## Step 1 — Create a project & key (2 minutes)
Sign up at [memclaw.net](https://memclaw.net) and create a **project** — that's your isolated tenant. In the project's *API keys* screen, mint a key (it starts with `mc_`). Treat the key like a password; it scopes every call to your project.
Export it so the commands below are copy-paste, then ask the platform who your key is — REST calls name your project explicitly with a `tenant_id`, and `whoami` returns yours:
```bash
export MEMCLAW_URL=https://memclaw.net
export MEMCLAW_KEY=mc_xxxxxxxxxxxxxxxxxxxx # your project API key
curl "$MEMCLAW_URL/api/v1/whoami" -H "X-API-Key: $MEMCLAW_KEY"
# {"tenant_id": "ten-7c41f2", "auth_mode": "tenant", "capabilities": ["read", "write"], ...}
export MEMCLAW_TENANT=ten-7c41f2 # yours, from the whoami response
```
Smoke-test the memory pipeline before wiring up any agents:
```bash
curl -X POST "$MEMCLAW_URL/api/v1/memories" \
-H "X-API-Key: $MEMCLAW_KEY" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "'$MEMCLAW_TENANT'", "agent_id": "quickstart", "content": "Our auth service uses JWT with 15-minute expiry."}'
```
> **Note** — Every MemClaw write is authored by an `agent_id` — that identity is what makes the fleet governance in Step 6 possible. On Cloud it's **required**: a gated deployment rejects an anonymous write with a `422`.
The write returns your stored memory immediately, with its `id`. Enrichment — classify, title, extract entities, scan for PII, detect contradictions, embed — runs asynchronously on *every* write and lands seconds later. Fetch the memory back and look at what MemClaw made of your one raw sentence — an LLM-inferred `memory_type`, `title`, `status`, and `weight` (importance):
```bash
curl "$MEMCLAW_URL/api/v1/memories/YOUR_MEMORY_ID?tenant_id=$MEMCLAW_TENANT" \
-H "X-API-Key: $MEMCLAW_KEY"
# "memory_type": "fact", "title": "Auth service uses JWT with 15-minute expiry",
# "weight": 0.8, "status": "confirmed", ...
```
Your agents never have to structure anything. Now search for it with completely different words:
```bash
curl -X POST "$MEMCLAW_URL/api/v1/search" \
-H "X-API-Key: $MEMCLAW_KEY" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "'$MEMCLAW_TENANT'", "query": "how long are login tokens valid", "top_k": 5}'
```
It comes back under an `items` array, each hit carrying its `similarity`, `title`, `agent_id`, and `visibility`:
```json
{ "items": [ {
"title": "Auth service uses JWT with 15-minute expiry",
"memory_type": "fact", "similarity": 0.55,
"agent_id": "quickstart", "visibility": "scope_team"
} ] }
```
Hybrid search blends vector semantic similarity, keyword matching, and knowledge-graph expansion — "login tokens" found "JWT" without sharing a single word. Memory layer: done. *(Keep queries compact and on-topic: retrieval has a similarity floor, so a long rambling question can score below it. REST `/search` caps `top_k` at 20; the `memclaw_recall` MCP tool your agents call has no such cap.)*
---
## Step 2 — Connect Claude Code via MCP (2 minutes)
Add MemClaw Cloud as an MCP server. Register it at **user scope** (`-s user`) so it's available in every directory — Step 5 runs Claude Code from three different folders, and the default `local` scope would register it only for the one you're standing in now:
```bash
claude mcp add --transport http -s user memclaw "$MEMCLAW_URL/mcp" \
--header "X-API-Key: $MEMCLAW_KEY"
```
Prefer a config file? Commit a `.mcp.json` to a project root — Claude Code reads that, but *not* `settings.json`. Keep your `mc_` key out of version control: reference an env var, or use the `-s user` command above.
```json
{
"mcpServers": {
"memclaw": {
"type": "http",
"url": "https://memclaw.net/mcp",
"headers": { "X-API-Key": "mc_xxxxxxxxxxxxxxxxxxxx" }
}
}
}
```
Confirm with `claude mcp list` — you should see `memclaw: ✓ Connected`. Claude Code now auto-discovers all 12 MemClaw tools: `memclaw_write`, `memclaw_recall`, `memclaw_list`, `memclaw_manage`, `memclaw_doc`, `memclaw_entity_get`, `memclaw_tune`, `memclaw_insights`, `memclaw_evolve`, `memclaw_stats`, `memclaw_keystones`, and `memclaw_keystones_set`.
---
## Step 3 — Install the skill (1 minute)
Tools tell an agent *what it can call*. A **skill** tells it *when and how* — the recall-before-acting habit, the write-after-learning habit, what makes a good memory versus noise. MemClaw Cloud serves its usage guide as a Claude Code skill straight from your endpoint:
```bash
curl -s "$MEMCLAW_URL/api/v1/install-skill" -H "X-API-Key: $MEMCLAW_KEY" > /tmp/install-memclaw-skill.sh
less /tmp/install-memclaw-skill.sh # always inspect before running
bash /tmp/install-memclaw-skill.sh
```
Verify and restart Claude Code (skills load at startup):
```bash
ls -la ~/.claude/skills/memclaw/SKILL.md
```
The skill is loaded on demand, not injected into every turn — it costs nothing until the agent actually reaches for memory. This step is the difference between an agent that *can* use memory and one that *does*.
---
## Step 4 — Give each agent an identity
Here's the move that turns "Claude Code with memory" into "a fleet."
Every MemClaw tool accepts an `agent_id` — the caller's identity. Agents auto-register on first write and get a trust tier. Memory authorship, retrieval tuning, and governance all hang off this identity. So each of our three agents needs to consistently identify itself.
The simplest mechanism in Claude Code is the project-level `CLAUDE.md` memory file. Create three working directories (or git worktrees), one per agent, and drop an identity block into each.
**`~/fleet/backend-dev/CLAUDE.md`:**
```markdown
# Agent identity
You are agent `backend-dev` in fleet `dev-fleet`.
On EVERY MemClaw tool call, pass agent_id="backend-dev" and fleet_id="dev-fleet".
# Memory discipline
- BEFORE starting any task: memclaw_recall for relevant context
(prior decisions, known gotchas, conventions).
- AFTER completing any task: memclaw_write what you learned —
decisions made, bugs found, anything a teammate would want to know.
- Write facts that age well. Skip transient noise.
```
**`~/fleet/code-reviewer/CLAUDE.md`:**
```markdown
# Agent identity
You are agent `code-reviewer` in fleet `dev-fleet`.
On EVERY MemClaw tool call, pass agent_id="code-reviewer" and fleet_id="dev-fleet".
# Memory discipline
- BEFORE reviewing: memclaw_recall the relevant architectural decisions
and conventions, so you review against what the team agreed — not
against your own taste.
- AFTER reviewing: memclaw_write recurring issues you flagged, so the
fleet stops repeating them.
```
**`~/fleet/docs-writer/CLAUDE.md`:**
```markdown
# Agent identity
You are agent `docs-writer` in fleet `dev-fleet`.
On EVERY MemClaw tool call, pass agent_id="docs-writer" and fleet_id="dev-fleet".
# Memory discipline
- BEFORE writing docs: memclaw_recall what backend-dev and code-reviewer
recorded about the feature. Docs describe what was BUILT, not what
was planned.
- AFTER writing: memclaw_write a pointer to what you documented.
```
That's the entire orchestration layer. No message bus, no LangGraph, no shared scratchpad files. Identity plus a shared memory substrate.
---
## Step 5 — Watch knowledge flow
Open three terminals, run `claude` in each agent's directory, and try the canonical loop.
### Terminal 1 (backend-dev):
> "Add rate limiting to the payments API. We decided on 100 req/min per key using a sliding window in Redis — record the decision and any gotchas you hit."
The agent does the work, then calls `memclaw_write` with something like *"Payments API rate limiting: 100 req/min per API key, sliding-window algorithm in Redis. Gotcha: the Redis connection pool maxes at 10 — raise REDIS_POOL_SIZE before adding more middleware that touches Redis."* MemClaw classifies it (a `decision` and a `rule`, probably), extracts entities (Redis, payments API), embeds it, and stamps it `scope_team` — visible to the whole fleet by default.
### Terminal 2 (code-reviewer), later, reviewing an unrelated PR that touches Redis:
> "Review this PR that adds a Redis-backed feature-flag cache."
Before opining, the agent calls `memclaw_recall("Redis conventions and known issues")` — and gets backend-dev's pool-size warning. The review comes back with: *"Heads up — the Redis connection pool is capped at 10 and the rate limiter already consumes connections; bump REDIS_POOL_SIZE or this will starve under load."*
The reviewer never saw that code being written. It never spoke to backend-dev. **One agent discovered; another recalled.** That's the moment the fleet stops being three isolated amnesiacs and starts compounding.
### Terminal 3 (docs-writer):
> "Document the payments API rate limiting."
It recalls both the original decision *and* the review note, and produces docs that match reality — including the operational caveat that exists nowhere in the code comments. And because this is Cloud, you can open **Prism** — your hosted dashboard — right now and watch these three memories appear, with their authors, scopes, and the entity graph drawn between them.
---
## Step 6 — Governance: enforced, not theoretical
Shared memory without permissions is a liability. On Cloud, three MemClaw mechanisms are live from your first write, **enforced per credential** — your `mc_` key only does what its trust tier allows.
**Visibility scopes.** Every memory is stamped at write time: `scope_agent` (private to the author), `scope_team` (fleet-wide — the default), or `scope_org` (cross-fleet). When backend-dev writes a half-baked hypothesis it isn't sure about yet, it keeps it `scope_agent` until confirmed. The boundary is enforced in the query layer — a private memory simply isn't returned to a caller who isn't its author.
**Trust tiers.** Agents carry a trust level (0–3) controlling cross-agent reads, writes, and deletes. New agents auto-register at a baseline; you promote them deliberately from the dashboard (*Agents → trust*), or over the API:
```bash
curl -X PATCH "$MEMCLAW_URL/api/v1/agents/backend-dev/trust?tenant_id=$MEMCLAW_TENANT" \
-H "X-API-Key: $MEMCLAW_KEY" \
-H "Content-Type: application/json" \
-d '{"trust_level": 2}'
```
A trust-1 agent manages its own memories; **cross-agent and fleet-wide operations require trust 2+**. A compromised or buggy low-trust agent simply *can't* reach the fleet's shared knowledge to corrupt or delete it.
**Keystones.** Scopes and trust govern *memories*; keystones govern *behavior*. A keystone is a mandatory policy every agent in scope reads at session start and must obey — even when it conflicts with the agent's own prompt or a user's instruction. Set one in the dashboard, or with the `memclaw_keystones_set` tool (a fleet-wide rule needs trust 2+). Your agents pull the scope-merged rule set with `memclaw_keystones` at the start of every session.
**The audit log & PII.** Every write, recall, transition, delete, trust change, and keystone edit is logged with agent and scope context — browse it in the dashboard when someone asks *"why does the docs agent believe X?"* And MemClaw scans every write for PII during enrichment, stamping `contains_pii` / `pii_types` on the memory so sensitive content is flagged on the way *in*, not discovered after a leak.
> **Prism, from day one** — Everything above is visible in **Prism**, the hosted dashboard — browse and search memories, walk the knowledge graph, drive status transitions, promote agent trust, and read the audit trail. It's there the moment you create a project.
---
## Step 7 — Close the loop: a fleet that learns and grooms itself
So far the fleet *shares* and is *governed*. MemClaw's third pillar is that it *improves* — and on Cloud the maintenance side runs on a schedule you don't manage.
**Outcome reporting (the Karpathy Loop).** After acting on recalled memories, an agent reports back via `memclaw_evolve` — success, failure, or partial, with the memory IDs that influenced the action. Successes reinforce memory weights; failures lose weight *and* auto-generate a preventive `rule` so the fleet doesn't repeat the mistake. To reinforce a *teammate's* memory (not just your own), report with `scope="fleet"` and hold trust 2+. Add one line to each `CLAUDE.md`: *"After acting on recalled memories, report the outcome with memclaw_evolve."*
**Per-agent retrieval tuning.** `memclaw_tune` lets each agent shape its own retrieval profile — your code-reviewer wants precision (higher `min_similarity`, lower `top_k`); your docs-writer wants breadth (more graph hops, more results). Search quality compounds per agent, per feedback signal.
**Hygiene runs automatically.** The crystallizer merges near-duplicate memories into canonical facts with provenance; contradiction detection supersedes stale facts when the team changes its mind; an 8-status lifecycle retires what's outdated. On Cloud these sweeps are scheduled for you — and `memclaw_insights` (focus `contradictions`, `stale`, `patterns`, …) lets an agent reflect over the corpus on demand, writing its findings back as `insight` memories the next agent will recall.
The result is a flywheel: **write → recall → act → report → better recall.** Every interaction makes the next one smarter — and you watch the weights move and the graph grow in Prism.
---
## Where to go from here
You now have a working three-agent fleet with shared, governed, self-improving memory — built entirely from a key and config files. Scaling paths:
- **More agents** is just more `CLAUDE.md` identities. The pattern is identical at 3 or 300.
- **Mixed fleets:** anything that speaks MCP joins the same brain — Cursor, Windsurf, Claude Desktop, Codex, custom agents via REST.
- **Mind the limits:** Cloud applies per-project rate limits, and the [memclaw.net](https://memclaw.net) free tier covers 10K memories. Batch your writes and searches within them — Prism shows current usage.
- **Run it yourself:** the same engine is open source. The [self-hosted OSS series](/docs/tutorials/multi-agent-fleet) builds this exact fleet on your own Docker stack.
---
**MemClaw Cloud** — managed, governed memory for agent fleets, operated for you, with the Prism dashboard included. Start on the free tier at **[memclaw.net](https://memclaw.net)**: 10K memories.
Your agents are already smart. Stop making them start from zero.
# Content Policy: PII screening & the business-vs-personal gate
URL: https://memclaw.net/docs/tutorials/content-policy-setup
Description: Configure the dashboard's Content Policy tab — screen every write for PII, PCI & secrets, and keep personal content out of the shared corporate graph.
Governed memory means deciding what should *never land* in the shared store in the first place. Two questions are worth asking on every write:
- **Does this leak PII?** An email, a card number, an API key copied into a memory crosses a team boundary and becomes a recall away from anyone.
- **Is this even business content?** A personal aside ("remind me to book a dentist") doesn't belong in the corporate knowledge graph at all.
The **Content Policy** tab answers both, per organization, at write time. This guide walks through every control, what each one does, and a sensible starting configuration.
> **Where it lives:** the dashboard's **Manage → Content Policy** tab. It has two sub-tabs: **Policy** (the controls below) and **Recent Actions** (the audit trail of what got flagged, masked, or dropped).
>
> **Everything is opt-in.** A fresh organization screens *nothing* — both gates default to off. You turn on what you need; nothing changes silently.
>
> **Give it a few minutes.** Settings are cached per worker, so a change can take a short while to take effect across every instance.
---
## Step 1 — Enable PII screening
The **PII, PCI & Secrets** card has one master switch — **Enable PII screening** (off by default: *"Off = no screening. On = screen every write per the action below."*). Turn it on, then pick what happens when something is detected.
### Action on detection
| Action | What it does |
|---|---|
| **Flag only** | Stores the memory with a `contains_pii` warning + an audit entry. Nothing is blocked or altered — a safe way to *measure* exposure before you enforce. (The default when you first enable screening.) |
| **Mask** | Redacts the sensitive spans in place and stores the rest. `jane@acme.com` becomes a redacted span; the surrounding memory survives. |
| **Drop** | Rejects the whole write — nothing persists. The caller gets a `422`. |
A good rollout is **Flag only → review Recent Actions for a week → switch to Mask or Drop** once you see what your agents actually write.
### Categories
Seven detectors, each a checkbox: **Email addresses · Phone numbers · Payment cards (PCI) · IBAN / bank accounts · National IDs (SSN, etc.) · API keys · Secrets / credentials.**
Leave them **all unchecked to screen everything** — that's the secure default (*"None selected — all categories are screened"*). Only narrow the set if you have a deliberate reason to ignore a category.
> **Two detection paths, one setting.** PII is caught two ways: a **deterministic, span-aware** scanner (regex + Luhn/IBAN/entropy validation) that runs before enrichment and is the only path precise enough to *mask*; and a **free-form LLM signal** during enrichment that catches PII phrased in prose. The free-form path's recall is only as good as your enrichment model — if you rely on **Mask/Drop** for free-form text, configure a capable `enrichment.model`. When the LLM path can't pinpoint spans, **Mask** falls back to **Flag**.
---
## Step 2 — Enable the business-vs-personal gate
The **Business vs Personal** card classifies each write as *business* or *personal* and applies a disposition to the personal ones. Flip **Enable business-vs-personal gate** on (*"Off = store everything. On = apply the disposition below to personal content."*), then choose:
| Non-business disposition | What happens to personal content |
|---|---|
| **Store normally** | No filtering — the classification is recorded for visibility, nothing else. |
| **Keep private** | Retained only in the **creating agent's** scope (`scope_agent`) — invisible to the team/fleet. |
| **Drop** | Not stored at all — keeps the corporate graph clean. |
"Business" is work, projects, customers, code, decisions, operations — anything an organization keeps in shared memory. "Personal" is private individual matters unrelated to work. The classifier returns one or the other on every write.
---
## Step 3 — (Optional) Turn on the fast pre-gate
The accurate backstop runs *after* enrichment. The **fast pre-gate** is an optimization that runs *before* it — a cheap classifier that rejects confidently-personal content **before** you pay to enrich and embed it. It only matters for the **Drop** disposition, so its controls light up only when **Drop** is selected (otherwise you'll see *"Applies to the Drop disposition only — ignored for the current one"*).
- **Enable fast pre-gate** — *"Reject confident personal content before enrichment/embedding — cheaper and faster than the post-enrichment gate, which stays the accurate backstop."* Pure cost/latency win; the post-enrichment gate still catches anything the fast pass lets through.
- **Fail closed when the classifier is unavailable** — this is the compliance lever, and it's worth understanding:
- **Off (the default) — fail open.** If the pre-gate classifier can't run, the write is **stored** and the post-enrichment gate is relied on as the backstop. Availability over strictness.
- **On — fail closed.** If the classifier can't run, the write is **rejected with a `503`**. Strictness over availability.
Treat the pre-gate as a hard control? **Turn fail-closed on** — otherwise a classifier outage silently lets personal content through the fast path. Want writes to never blocked by an LLM hiccup? Leave it off.
---
## Step 4 — Watch it work: Recent Actions
The **Recent Actions** sub-tab is the audit trail of every enforcement decision — so "what is the policy actually catching?" is a tab, not a guess. You'll see entries like:
- `pii_flag` / `pii_mask` / `pii_drop` — PII detected and handled per your action.
- `nonbusiness_keep_private` / `nonbusiness_drop` — personal content scoped down or rejected by the post-enrichment gate.
- `nonbusiness_pregate_drop` — rejected early by the fast pre-gate.
- `nonbusiness_pregate_unavailable` — a fail-closed `503` because the classifier was down.
This is where you confirm a rollout before tightening it — start in **Flag** / **Store**, read the log, then enforce.
---
## A sensible starting point
| Setting | Start with | Tighten to |
|---|---|---|
| PII screening | **On**, **Flag only**, all categories | **Mask** (or **Drop** for regulated data) |
| Categories | all (none checked) | narrow only with a reason |
| Business-vs-personal | **On**, **Store normally** | **Keep private** or **Drop** |
| Fast pre-gate | off | **On** once you move to **Drop** |
| Fail closed | off | **On** if the gate is a compliance control |
Enforce only after Recent Actions shows you what real traffic looks like.
---
## Configuring it without the dashboard
The tab edits per-organization settings in `core-api`. Self-hosted? Set the same keys directly via `PUT /api/settings?tenant_id=` with a partial `governance` block (unsent keys are left unchanged):
```json
{
"governance": {
"pii": {
"enabled": true,
"action": "mask",
"categories": { "email": true, "credit_card": true }
},
"non_business": {
"enabled": true,
"disposition": "drop",
"pregate": { "enabled": true, "fail_closed": true }
}
}
}
```
Defaults are opt-in (`enabled` is `false` everywhere). Stick to the allowed values or the save is rejected with a `422`:
| Key | Type / allowed values | Default |
|---|---|---|
| `governance.pii.enabled` | `bool` | `false` |
| `governance.pii.action` | `"flag"` \| `"mask"` \| `"drop"` | `"flag"` |
| `governance.pii.categories.` — one per category (`email`, `phone`, `credit_card`, `iban`, `national_id`, `api_key`, `secret`) | `bool` | `false` (none ⇒ all screened) |
| `governance.non_business.enabled` | `bool` | `false` |
| `governance.non_business.disposition` | `"store"` \| `"keep_private"` \| `"drop"` | `"store"` |
| `governance.non_business.pregate.enabled` | `bool` | `false` |
| `governance.non_business.pregate.fail_closed` | `bool` | `false` (fail-open) |
---
## What you've got
Two gates on the front door of shared memory, both off until you ask for them:
- **PII screening** — flag, mask, or drop emails, cards, IBANs, national IDs, keys, and secrets, caught before they cross a boundary.
- **The business-vs-personal gate** — keep personal content out of the corporate graph, with an optional fast pre-gate and a fail-open/closed choice for when the classifier is down.
- **Recent Actions** — the audit trail that turns "is the policy working?" into something you can read.
For the concepts behind this — scopes, trust tiers, keystones, and where PII detection fits — see **[Governed memory](/docs/tutorials/governance-keystones)** and the **[Governance concept guide](/docs/concepts/governance)**.
---
**caura-memclaw · Apache 2.0** — governed memory in the open. ⭐ [Star on GitHub](https://github.com/caura-ai/caura-memclaw) · [Join Discord](https://discord.com/invite/aNfpgfpj) · [memclaw.net](https://memclaw.net)
# Your first memories on MemClaw Cloud
URL: https://memclaw.net/docs/tutorials/first-memories
Description: The five-minute Cloud starter — sign up, mint a key, write three memories, recall them with completely different words, and see them in Prism.
**Managed (Cloud) starter — the smallest end-to-end tutorial.** You just signed up and want to see memory work before wiring up any agents. Ready for a real multi-agent build afterwards? That's [Build an agent fleet on MemClaw Cloud](/docs/tutorials/cloud-fleet). Prefer to run MemClaw yourself? Start with the [self-hosted OSS series](/docs/tutorials/multi-agent-fleet).
You just signed up at [memclaw.net](https://memclaw.net) — or you're about to. Before wiring up agents, fleets, or MCP configs, you should see the core trick with your own eyes: **you write a plain sentence, and MemClaw turns it into enriched, searchable memory you can find again with completely different words.**
That's this tutorial. No SDK, no install, no agent harness — just a terminal with `curl`. In five minutes you will:
1. Sign up and mint an API key
2. Write three memories
3. Recall them with words they don't contain
4. See them in **Prism**, your hosted dashboard
Everything you do here by hand is exactly what an AI agent does automatically over MCP — so when you graduate to the [fleet tutorial](/docs/tutorials/cloud-fleet), nothing will feel like magic.
---
## The idea in one picture
```
"We chose PostgreSQL over MongoDB." ← you write plain text
│
▼
┌─────────────────────┐
│ MemClaw Cloud │
│ classify · title │
│ entities · weight │
│ PII scan · embed │
└─────────────────────┘
│
▼
"which database did we pick and why?" ← recalled by meaning,
│ not by keywords
▼
your memory, ranked first
```
One raw sentence in; a classified, titled, embedded, governed memory out. Recall works on *meaning* — the word "database" appears nowhere in what you wrote.
---
## Step 1 — Sign up & mint a key (2 minutes)
Sign up at [memclaw.net](https://memclaw.net) and create a **project** — that's your isolated tenant. In the project's *API keys* screen, mint a key (it starts with `mc_`). Treat the key like a password; it scopes every call to your project.
Export it so the commands below are copy-paste:
```bash
export MEMCLAW_URL=https://memclaw.net
export MEMCLAW_KEY=mc_xxxxxxxxxxxxxxxxxxxx # your project API key
```
Confirm the platform is reachable:
```bash
curl "$MEMCLAW_URL/api/v1/health" -H "X-API-Key: $MEMCLAW_KEY"
# {"status": "ok", "storage": "connected", "redis": "connected", "event_bus": "ok"}
```
REST calls name your project explicitly with a `tenant_id`. Ask the platform who your key is — `whoami` returns it:
```bash
curl "$MEMCLAW_URL/api/v1/whoami" -H "X-API-Key: $MEMCLAW_KEY"
# {"tenant_id": "ten-7c41f2", "auth_mode": "tenant", "capabilities": ["read", "write"], ...}
export MEMCLAW_TENANT=ten-7c41f2 # yours, from the whoami response
```
---
## Step 2 — Write your first memory
A memory is natural-language `content` plus two identifiers: `tenant_id` (your project) and `agent_id` (whoever is writing). You're not an agent yet, so call yourself `me`:
```bash
curl -X POST "$MEMCLAW_URL/api/v1/memories" \
-H "X-API-Key: $MEMCLAW_KEY" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "'$MEMCLAW_TENANT'", "agent_id": "me", "content": "I prefer concise answers, dark mode, and metric units."}'
```
> **Note** — `agent_id` is **required** on Cloud. Leave it out and the write is rejected with a `422`: *"agent_id is required; only the standalone single-tenant deployment may omit it."* Later, when real agents write memories, this identity is what governance hangs off.
The response returns your stored memory immediately — with an `id` you'll use in a moment. Enrichment — classify, title, extract entities, scan for PII, embed — runs asynchronously and lands seconds later. You never structure anything yourself; we'll fetch the enriched result after the next step.
---
## Step 3 — Write two more, then look at what MemClaw did
Memories get interesting in variety. Write a **decision** and a **gotcha**:
```bash
curl -X POST "$MEMCLAW_URL/api/v1/memories" \
-H "X-API-Key: $MEMCLAW_KEY" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "'$MEMCLAW_TENANT'", "agent_id": "me", "content": "We chose PostgreSQL over MongoDB for the orders service because we need transactions."}'
curl -X POST "$MEMCLAW_URL/api/v1/memories" \
-H "X-API-Key: $MEMCLAW_KEY" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "'$MEMCLAW_TENANT'", "agent_id": "me", "content": "Gotcha: the staging environment resets every Sunday night — do not leave test data there."}'
```
Give enrichment a few seconds, then fetch the decision back by the `id` from its response:
```bash
curl "$MEMCLAW_URL/api/v1/memories/YOUR_MEMORY_ID?tenant_id=$MEMCLAW_TENANT" \
-H "X-API-Key: $MEMCLAW_KEY"
```
Your one raw sentence now looks like this:
```json
{
"memory_type": "decision",
"title": "Chose PostgreSQL over MongoDB for orders service",
"weight": 0.85,
"status": "confirmed",
"entity_links": [
{ "canonical_name": "postgresql", "entity_type": "technology" },
{ "canonical_name": "mongodb", "entity_type": "technology" },
{ "canonical_name": "orders service", "entity_type": "project" },
{ "canonical_name": "transactions", "entity_type": "concept" }
]
}
```
An LLM classified it as a `decision`, titled it, weighted its importance, and extracted four linked entities — from a sentence you typed in five seconds. Fetch your other two memories and compare: the preference and the warning are classified differently, and that classification later shapes how each one is ranked, maintained, and retired.
> **Tip** — Writing the *exact same content* twice returns a `409`: MemClaw deduplicates instead of storing copies. Your memory stays clean by default.
---
## Step 4 — Recall with different words
Here's the point of all this. Ask about the database decision *without using any of its words*:
```bash
curl -X POST "$MEMCLAW_URL/api/v1/search" \
-H "X-API-Key: $MEMCLAW_KEY" \
-H "Content-Type: application/json" \
-d '{"tenant_id": "'$MEMCLAW_TENANT'", "query": "which database did we pick and why", "top_k": 5}'
```
The decision comes back first, under an `items` array with a `similarity` score:
```json
{ "items": [ {
"title": "Chose PostgreSQL over MongoDB for orders service",
"memory_type": "decision", "similarity": 0.67,
"agent_id": "me", "visibility": "scope_team"
} ] }
```
Look at what just happened: the query says "database" — your memory never does. It says "pick" — you wrote "chose". Vector semantic similarity, blended with keyword matching and knowledge-graph expansion, bridges the gap. Try `"UI preferences"` next — it finds your dark-mode memory the same way. *(Keep queries compact and on-topic: retrieval has a similarity floor, so a long rambling question can score below it. REST `/search` caps `top_k` at 20; the `memclaw_recall` MCP tool agents use has no such cap.)*
---
## Step 5 — See it in Prism
Open your dashboard at [memclaw.net](https://memclaw.net). **Prism** is live from your very first write: your three memories are there with their inferred types, titles, and weights; the entities extracted from them (PostgreSQL, MongoDB, the orders service); and an audit entry for every call you just made.
That's the whole loop: **write plain text → enriched automatically → recalled semantically → visible and governed in Prism.**
---
## Bonus — let your AI assistant do it
Everything above is what an AI agent does on its own over MCP. Paste this into your client config (Claude Desktop, Claude Code, Cursor, Windsurf):
```json
{
"mcpServers": {
"memclaw": {
"url": "https://memclaw.net/mcp",
"headers": { "X-API-Key": "mc_xxxxxxxxxxxxxxxxxxxx" }
}
}
}
```
Restart the client, then say:
> "Remember that I prefer concise answers and dark mode."
Later, in a *fresh session*:
> "What do you know about my preferences?"
The agent calls `memclaw_write` and `memclaw_recall` for you — the same endpoints you just hit by hand. Same memories, same project, same dashboard.
---
## Where to go from here
You've seen the primitive: plain text in, semantic recall out, everything visible in Prism. Next steps:
- **[Build an agent fleet on MemClaw Cloud](/docs/tutorials/cloud-fleet)** — the natural next step: three Claude Code agents sharing this same memory, with identities, trust tiers, and governance.
- **[Memory pipeline concepts](/docs/concepts/memory-pipeline)** — what actually happens to a sentence between write and recall.
- **Mind the free tier** — 10K memories, 5K writes and 500 recalls per month; Prism shows current usage.
---
**MemClaw Cloud** — managed, governed memory for agent fleets, operated for you, with the Prism dashboard included. Start on the free tier at **[memclaw.net](https://memclaw.net)**: 10K memories.
Your first three memories took five minutes. Your agents' next ten thousand will take none of your time at all.
# Governed memory: scopes, trust tiers & keystone policies
URL: https://memclaw.net/docs/tutorials/governance-keystones
Description: Who sees what, who can change the fleet's knowledge, and what every agent must obey — visibility scopes, trust tiers, and keystone policies.
In [Part 1](/docs/tutorials/multi-agent-fleet) we gave three agents one shared memory; in [Part 2](/docs/tutorials/memory-dashboard) we got eyes on it. Both parts leaned on a word we mostly hand-waved: *governed*.
Here's why it matters. The moment more than one agent can **write** to a shared store, you've created a blast radius. A buggy agent can scribble nonsense the others recall as fact. A compromised one can delete the fleet's hard-won knowledge. A careless one can leak a customer's email into a memory that crosses a team boundary. "Point three agents at a shared vector store and hope" is not a memory architecture — it's an incident waiting for a date.
Governance is what makes shared memory *deployable* instead of merely possible. MemClaw answers three questions, on every operation:
- **Who can see this memory?** → visibility scopes
- **Who is allowed to change the fleet's knowledge?** → trust tiers
- **What must every agent obey, no matter what its prompt says?** → keystones
> Commands below use the default `http://localhost:8000` and `X-API-Key: standalone` from Part 1. Everything has an MCP-tool equivalent your agents call directly; the REST calls just make the behavior easy to see.
---
## Step 1 — Visibility scopes: who can see a memory
Every memory is stamped with a visibility at write time:
- **`scope_agent`** — private to the author.
- **`scope_team`** — visible to the whole fleet. **The default.**
- **`scope_org`** — visible across fleets (cross-fleet recall is permissioned, not open).
The useful one to understand is `scope_agent`. When `backend-dev` has a half-baked hunch it isn't ready to broadcast, it writes it private:
```bash
curl -X POST http://localhost:8000/api/v1/memories \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"tenant_id":"default","agent_id":"backend-dev","fleet_id":"dev-fleet",
"visibility":"scope_agent",
"content":"Unconfirmed: the p99 latency spikes may be GC pauses in the embedding worker. Profile before sharing."}'
```
Now watch the boundary hold. Listing memories *as* `code-reviewer` does **not** surface another agent's private memory; listing *as the author* does:
```bash
# as code-reviewer — private memory is invisible
curl "http://localhost:8000/api/v1/memories?tenant_id=default&agent_id=code-reviewer"
# → scope_agent memories: NONE
# as backend-dev (the author) — it's there
curl "http://localhost:8000/api/v1/memories?tenant_id=default&agent_id=backend-dev"
# → scope_agent memories: ["Hypothesis: p99 latency spikes ..."]
# with no agent_id at all — scope_agent is hidden
curl "http://localhost:8000/api/v1/memories?tenant_id=default"
# → scope_agent memories: NONE
```
Visibility isn't a tag the UI respects on its honor — it's enforced in the query layer. A `scope_agent` row simply isn't returned to a caller who isn't its author. (The `?agent_id=` above filters the listing by *author*; the caller identity visibility is actually enforced against is the `X-Agent-ID` header from the keystone step — for `scope_agent` the two coincide, since only the author can see the row.) When the hunch is confirmed, the agent promotes it with a one-line update (`PATCH /memories/{id}`, or `memclaw_manage op=update` from an agent):
```bash
curl -X PATCH "http://localhost:8000/api/v1/memories/?tenant_id=default" \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"visibility":"scope_team"}'
# → visibility now: scope_team
```
…and the fleet starts recalling it.
---
## Step 2 — Trust tiers: who can change the fleet's knowledge
Every agent carries a **trust level, 0–3**. New agents auto-register at a baseline (trust 1) on their first write; you promote them deliberately:
```bash
curl -X PATCH http://localhost:8000/api/v1/agents/backend-dev/trust \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"trust_level": 2}'
# → {"agent_id":"backend-dev","trust_level":2,...}
```
The tiers gate increasingly dangerous operations. A trust-1 agent manages its own memories; **cross-agent and fleet-wide operations require trust 2+**. A compromised or buggy low-trust agent simply *can't* reach the fleet's shared knowledge to corrupt it.
> **Caller identity vs. authorship.** A memory's *author* is the `agent_id` in the request body. A trust-gated *operation* is checked against the **calling** agent — set by the `X-Agent-ID` header (the enterprise gateway injects this per credential; in local standalone you pass it yourself). Keep that distinction in mind for the next step.
---
## Step 3 — Keystones: the rules the fleet must obey
Scopes and trust govern *memories*. **Keystones** govern *behavior*. A keystone is a mandatory policy — a rule every agent in scope reads at the start of a session and must obey, **even when it conflicts with the agent's own prompt or a user's instruction.** This is the mechanism most "shared memory" systems don't have, and it's why MemClaw can be trusted with a fleet rather than a single chatbot.
A trust-2 agent sets a fleet-wide rule (note: a `scope=fleet` keystone carries no `agent_id` — the authorizing identity comes from `X-Agent-ID`):
```bash
curl -X POST http://localhost:8000/api/v1/memclaw/keystones \
-H "X-API-Key: standalone" -H "X-Agent-ID: backend-dev" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"default","fleet_id":"dev-fleet","doc_id":"redis-pool-guardrail",
"title":"Redis pool guardrail","scope":"fleet","weight":"high",
"content":"Never raise REDIS_POOL_SIZE above 50 without a load test signed off in #infra - the payments rate limiter already consumes the shared pool."}'
# → 200 set: redis-pool-guardrail | scope fleet | weight high
```
> **Heads-up — caller identity.** Trust on keystones is checked against the *calling* agent, not the body. In standalone the API-key holder is the trusted operator, so the write above **succeeds without** an `X-Agent-ID` header — you're authoring as that operator. Pass `X-Agent-ID: ` only when you want to author *as* a specific agent; an identity asserted by the header alone is treated as **unverified**, so fleet/tenant-scoped rules then require that agent to be registered at **trust ≥ 2** (e.g. `X-Agent-ID: quickstart` on a trust-1 agent returns `403 "… (trust_level=1) < required 2."`). The enterprise gateway sets a *verified* `X-Agent-ID` per credential automatically.
Every agent in `dev-fleet` now reads it — the agent-native call is `memclaw_keystones` (it returns the **scope-merged** set: tenant + fleet + the agent's own rules), or over REST:
```bash
curl "http://localhost:8000/api/v1/memclaw/keystones?tenant_id=default&fleet_id=dev-fleet" \
-H "X-API-Key: standalone" -H "X-Agent-ID: code-reviewer"
# → [ { "doc_id":"redis-pool-guardrail", "data":{ "scope":"fleet",
# "weight":100, "content":"Never raise REDIS_POOL_SIZE above 50 ..." } } ]
```
(The `high` you set comes back as `100` — keystone weight buckets to `low` / `med` / `high` on input and is stored as a number.)
The skill teaches agents to call `memclaw_keystones` **once at session start and obey what it returns** — those rules outrank conflicting user instructions. That's the whole point: a policy that lives in governed memory, not in a system prompt an agent can be talked out of.
**Trust is enforced on keystones, tiered by the rule's reach:**
| Keystone scope | Who may author it |
|---|---|
| `scope=agent` (your own) | trust ≥ 1 |
| `scope=fleet` / `scope=tenant` / another agent's | trust ≥ 2 |
So when `quickstart` (trust 1) tries to author a fleet-wide policy, MemClaw refuses:
```bash
curl -X POST http://localhost:8000/api/v1/memclaw/keystones \
-H "X-API-Key: standalone" -H "X-Agent-ID: quickstart" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"default","fleet_id":"dev-fleet","doc_id":"sneaky-rule",
"title":"Sneaky","scope":"fleet","weight":"high","content":"..."}'
# → 403 "Agent 'quickstart' (trust_level=1) < required 2."
```
A low-trust agent can't quietly install a fleet-wide policy. That's governance you can deploy.
---
## Step 4 — The audit log: provenance, not a shrug
Every write, recall, transition, delete, trust change, and keystone edit is logged with tenant, agent, and scope context:
```bash
curl "http://localhost:8000/api/v1/audit-log" -H "X-API-Key: standalone"
# → [ {action:"agent_registered", agent:"backend-dev", ...},
# {action:"create", agent:"backend-dev", ...},
# {action:"entity_extraction", ...}, ... ]
```
When someone asks *"why does the docs agent believe X?"* — or *"who set that guardrail?"* — you have a provenance chain, not a shrug.
---
## Step 5 — PII: caught at the door
Governance includes not writing the wrong thing into shared memory in the first place. MemClaw scans every write for PII during enrichment and stamps the result on the memory:
```bash
# writing a memory that mentions a customer's email + phone
curl -X POST http://localhost:8000/api/v1/memories ... \
-d '{... "content":"Customer escalation: Jane Doe (jane.doe@acme.com, +1-415-555-0142) reported ..."}'
# the stored memory's metadata:
# "contains_pii": true,
# "pii_types": ["email", "phone", "name"]
```
In the OSS engine this is **detection**: the memory is flagged (`contains_pii` / `pii_types`) but stored as-is — OSS does not redact or block it. **Enforcement** — quarantining or refusing a flagged memory before it crosses a team or org boundary — is the enterprise gateway's job. Either way, the signal is captured on the way *in*, not discovered after a leak.
---
## What you've got
None of this required configuration. Scopes, trust tiers, keystones, the audit log, and PII detection are built in, not bolted on — which is exactly the argument for a *governed* memory layer over a shared vector store and good intentions:
- **Scopes** decide who can see a memory.
- **Trust tiers** decide who can change the fleet's knowledge.
- **Keystones** decide what every agent must obey, prompt or no prompt.
- **The audit log** answers "why / who," after the fact.
- **PII detection** stops the wrong thing entering shared memory.
**Next — [Part 4: The Karpathy Loop](/docs/tutorials):** governed memory that also *learns* — agents report outcomes, successful memories gain weight, and failures auto-generate preventive rules.
---
**caura-memclaw · Apache 2.0** — governance is in the open-source engine, not an upsell: scopes, trust tiers, keystones, audit, PII. ⭐ [Star on GitHub](https://github.com/caura-ai/caura-memclaw) · [Join Discord](https://discord.com/invite/aNfpgfpj) · [memclaw.net](https://memclaw.net)
Shared memory without governance is a liability. With it, it's infrastructure.
# Tutorials
URL: https://memclaw.net/docs/tutorials
Description: Hands-on tutorials for building governed, shared, self-improving memory for AI agent fleets — on managed MemClaw Cloud or self-hosted OSS.
These tutorials are **self-contained, end-to-end builds** — each one stands on its own. (Just want to connect a single agent in five minutes? That's the [Quickstart](/docs/getting-started/quickstart), not a tutorial.)
There are **two tracks**, depending on where your memory lives. They build the same three-agent fleet and teach the same primitives — the only difference is who runs the engine.
## MemClaw Cloud (managed)
No infrastructure to run. Start with the five-minute first-memories tutorial, then graduate to the full fleet build — both run against the hosted platform at [memclaw.net](https://memclaw.net) using your `mc_` API key, with the result visible in the **Prism** dashboard.
- **[Your first memories on MemClaw Cloud](/docs/tutorials/first-memories)** — the five-minute starter for a brand-new account: sign up, mint a key, write three memories with `curl`, recall them with different words, and see them in Prism.
- **[Build an agent fleet on MemClaw Cloud](/docs/tutorials/cloud-fleet)** — sign up, connect Claude Code over MCP, give each agent an identity, and watch governed memory flow between them — governance and hygiene run for you.
## The self-hosted (OSS) series
A hands-on, six-part series that builds the fleet on **a shared, governed MemClaw stack you run yourself** — from "hello fleet" to the advanced governance and self-improvement features. Every part runs against the same self-hosted stack, so the fleet's memory accumulates across the series instead of resetting each time.
1. **[Building a Multi-Agent Fleet with MemClaw and Claude Code](/docs/tutorials/multi-agent-fleet)** — spin up MemClaw, connect Claude Code over MCP, give each agent an identity, and watch knowledge flow between them.
2. **[The Memory Dashboard](/docs/tutorials/memory-dashboard)** — a browsable window into your fleet's memory: search, graph, audit, write, and govern, from one HTML file and a reverse proxy.
3. **[Governed memory: scopes, trust tiers & keystone policies](/docs/tutorials/governance-keystones)** — who sees what, who can change the fleet's knowledge, and the keystone policies every agent must obey.
4. **[The Karpathy Loop: memory that learns from outcomes](/docs/tutorials/karpathy-loop)** — report outcomes; memory reinforces what works and writes rules from what fails, and each agent tunes its own recall.
5. **[Memory hygiene at scale: contradictions, supersession & the crystallizer](/docs/tutorials/memory-hygiene)** — automatic contradiction detection and supersession, the 8-state lifecycle, the crystallizer hygiene scan, and `memclaw_insights`.
6. **[The knowledge graph: entities, relations & graph-boosted recall](/docs/tutorials/knowledge-graph)** — entity extraction, synonym resolution, evidence-carrying relations, and how graph-expansion lifts every recall.
## Focused guides
Standalone how-tos that don't depend on either track:
- **[Content Policy: PII screening & the business-vs-personal gate](/docs/tutorials/content-policy-setup)** — configure the dashboard's Content Policy tab to flag, mask, or drop PII, and keep personal content out of the shared corporate graph.
# The Karpathy Loop: memory that learns from outcomes
URL: https://memclaw.net/docs/tutorials/karpathy-loop
Description: Make shared memory improve with use — report outcomes, reinforce what works, auto-generate rules from failures, and tune retrieval per agent.
[Part 1](/docs/tutorials/multi-agent-fleet) gave the fleet shared memory; [Part 2](/docs/tutorials/memory-dashboard) gave us eyes on it; [Part 3](/docs/tutorials/governance-keystones) governed it. All three treat memory as a *store* — something you write to and read from.
This part makes it a *system that learns*. After an agent acts on a recalled memory, it reports the **outcome** — success, failure, or partial. Successful memories gain weight (and rank higher next time); failures lose weight **and** auto-generate a preventive `rule` so the fleet doesn't repeat the mistake. The project calls this the **Karpathy Loop**: `write → recall → act → report → better recall`.
> Commands use the default `http://localhost:8000` + `X-API-Key: standalone` from Part 1. `memclaw_evolve` and `memclaw_tune` are the agent-native MCP tools; the REST calls (`/evolve/report`, `/agents/{id}/tune`) make the behavior visible.
---
## Step 1 — Report an outcome
When an agent finishes acting on memories it recalled, it calls `memclaw_evolve` with the `outcome_type` (`success` | `failure` | `partial`), a natural-language description, and the `related_ids` — the memories that influenced the action.
```bash
curl -X POST http://localhost:8000/api/v1/evolve/report \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"tenant_id":"default","agent_id":"backend-dev","fleet_id":"dev-fleet",
"scope":"fleet","outcome_type":"success",
"related_ids":[""],
"outcome":"Shipped the payments rate limiter; held up under load tests with no false throttling."}'
```
Every report does three things: adjusts the weight of each related memory, records an `outcome` memory (so the report itself is auditable), and — on failure — may emit a `rule`.
---
## Step 2 — Success reinforces
A `success` nudges each related memory's weight **up by +0.1** (capped at 1.0):
```bash
# → "weight_adjustments":[{"memory_id":"...","old_weight":0.9,"new_weight":1.0,"delta":0.1}]
# "rules_generated":[]
```
No rule on success — correct; you only want preventive rules from things that went wrong. The reinforced memory now ranks higher in future recalls, so what worked surfaces first.
---
## Step 3 — Failure penalizes *and* writes a rule
This is the heart of the loop. Report a `failure` against the same decision:
```bash
curl -X POST http://localhost:8000/api/v1/evolve/report \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"tenant_id":"default","agent_id":"backend-dev","fleet_id":"dev-fleet",
"scope":"fleet","outcome_type":"failure","related_ids":[""],
"outcome":"Shipped the 100 rpm sliding-window limiter to prod; it caused false throttling for legitimate batch API clients during nightly jobs and we rolled it back. The per-key window is too blunt for bursty batch traffic."}'
```
Two things happen:
```json
{
"outcome_type": "failure",
"weight_adjustments": [
{ "memory_id": "...", "old_weight": 1.0, "new_weight": 0.85, "delta": -0.15 }
],
"rules_generated": [
{ "rule_memory_id": "...", "confidence": 0.78,
"condition": "IF a per-key rate limiter is a fixed-size sliding-window applied to bursty traffic (nightly batch jobs, backfills) where legitimate clients exceed the average rate in short intervals",
"action": "THEN use a burst-tolerant strategy (token-bucket / leaky-bucket, or a sustained-rate + burst-allowance tier); load-test with realistic batch patterns and roll out as a canary before full prod." } ]
}
```
1. **The weight drops by −0.15** — note the asymmetry: failures (−0.15) cost more than successes earn (+0.1), so a memory that burns the fleet decays faster than one good outcome can prop it up. (Partial outcomes nudge +0.03; weights floor at 0.05 and cap at 1.0.)
2. **A preventive `rule` memory is generated** (above a confidence threshold) — a structured `IF … THEN …` the fleet will now recall. It's a normal `scope_team` memory of type `rule`, titled e.g. *"Burst-tolerant rate limiting for bursty per-key traffic."*
So the next time any agent recalls "rate limiting," it gets both the original decision (now down-weighted) **and** the rule that says *don't do the blunt version for bursty traffic*. The fleet learned from one failure, once.
---
## Step 4 — Per-agent retrieval tuning
Different agents want different recall. A reviewer wants **precision** (few, high-confidence hits); a docs writer wants **breadth** (more results, more graph context). `memclaw_tune` lets each agent shape its own retrieval profile — `top_k`, `min_similarity`, `fts_weight` (semantic↔keyword), `graph_max_hops`, freshness decay, and recall-boost knobs.
```bash
# code-reviewer dials in precision
curl -X PATCH "http://localhost:8000/api/v1/agents/code-reviewer/tune?tenant_id=default" \
-H "X-API-Key: standalone" -H "X-Agent-ID: code-reviewer" -H "Content-Type: application/json" \
-d '{"min_similarity":0.55,"top_k":5}'
# → search_profile: {"top_k":5,"min_similarity":0.55}
# docs-writer dials in breadth
curl -X PATCH "http://localhost:8000/api/v1/agents/docs-writer/tune?tenant_id=default" \
-H "X-API-Key: standalone" -H "X-Agent-ID: docs-writer" -H "Content-Type: application/json" \
-d '{"graph_max_hops":2,"top_k":15}'
# → search_profile: {"top_k":15,"graph_max_hops":2}
```
Each agent's profile is applied to *its* recalls, so search quality compounds per agent, per feedback signal. A few notes:
- An agent tunes **its own** profile — `X-Agent-ID` identifies the caller; tuning a peer is blocked (except for admin/operator keys). *In standalone the `standalone` key is admin-equivalent, so that self-only guard is bypassed locally; it bites once each agent has its own credential behind the gateway.*
- `memclaw_tune` is also the agent-native MCP tool; `GET /agents/{id}/tune` reads the current profile.
- Agents register on their **first write**, not on recall. If an agent has only *recalled* so far, tuning it returns `404 "Agent not found"` — have it write one memory first (Part 1's `code-reviewer` records a note for exactly this reason), or tune an agent that already has, like `docs-writer`.
---
## Step 5 — Close the loop
Make reporting a habit, not an afterthought. One line in each agent's `CLAUDE.md` from Part 1:
```markdown
- After acting on recalled memories, report the outcome with memclaw_evolve
(success / failure / partial) and the memory IDs that influenced the action.
```
Now the flywheel turns on its own:
**`write → recall → act → report → better recall`**
Every interaction leaves the memory a little smarter than it found it. Successes float to the top; failures become guardrails; each agent's retrieval sharpens to its job. A store you only read and write is static. A store that learns from outcomes compounds — which is the whole reason to give a fleet shared memory in the first place.
**Next — [Part 5: Memory hygiene at scale](/docs/tutorials/memory-hygiene):** what keeps 20,000 accumulating memories from becoming a swamp — contradiction detection, supersession, and the crystallizer.
---
**caura-memclaw · Apache 2.0** — the Karpathy Loop (`evolve`) and per-agent tuning (`tune`) are in the open-source engine. ⭐ [Star on GitHub](https://github.com/caura-ai/caura-memclaw) · [Join Discord](https://discord.com/invite/aNfpgfpj) · [memclaw.net](https://memclaw.net)
A fleet that shares is useful. A fleet that learns is an asset.
# The knowledge graph: entities, relations & graph-boosted recall
URL: https://memclaw.net/docs/tutorials/knowledge-graph
Description: The graph view from Part 2 wasn't decoration — it's the substrate MemClaw builds on every write, and the reason recall finds memories you never lexically matched. Entity extraction, synonym resolution, evidence-carrying relations, and the graph-expansion search blend.
[Part 1](/docs/tutorials/multi-agent-fleet) gave the fleet shared memory; [Part 2](/docs/tutorials/memory-dashboard) gave us a graph view of it; [Parts 3](/docs/tutorials/governance-keystones)–[5](/docs/tutorials/memory-hygiene) governed it, evolved it, and kept it clean. One thing tied them all together silently: on **every** write, MemClaw was building a small typed graph of your domain — entities, their relations, and which memory each edge came from. This is what makes recall feel uncanny ("the reviewer never read that code, how did it know?"): the search doesn't just match text, it walks the graph.
> Commands use the default `http://localhost:8000` + `X-API-Key: standalone` from Part 1. Everything below was run against the fleet seeded through Parts 1–5.
---
## Step 1 — What gets extracted on every write
When `backend-dev` wrote the rate-limit decision back in [Part 1](/docs/tutorials/multi-agent-fleet), the response carried more than enrichment — it set off a background pass that pulled **entities** out of the prose and connected them with **typed relations**. Fetch any memory and the linkage is right there:
```bash
curl -s "http://localhost:8000/api/v1/memories/bb5746fc-...?tenant_id=default" \
-H "X-API-Key: standalone" | jq '.entity_links'
```
```json
[
{ "entity_id": "5d526d26-...", "role": "subject" }, // payments api
{ "entity_id": "07b7933e-...", "role": "mentioned" }, // redis
{ "entity_id": "016336b6-...", "role": "mentioned" }, // redis connection pool
{ "entity_id": "8bf57b15-...", "role": "mentioned" } // redis_pool_size
]
```
The pipeline is doing four things on the write path: it pulls candidate noun-phrases out of the content, classifies each (`technology`, `concept`, `project`, `person`, `org`, …), embeds the surface form, and either **resolves** it to an existing entity (cosine ≥ 0.85) or creates a new canonical row. Then it asks the LLM for **typed predicates** between the entities it just saw — `uses`, `implements`, `consumes_connections`, `pool_is_capped_at` — and writes them as edges with the **memory ID as evidence**. This is async (`embedding_pending: true` for a few seconds; see Part 5's note on lag), but it's running on **every** write, fact-or-decision-or-insight, without any setup.
There's nothing to configure. The model is the corpus.
---
## Step 2 — Synonym resolution: the engine knows "Redis cache" is just *redis*
Imagine `backend-dev` writes a new memory about Redis using a different name:
```bash
curl -X POST http://localhost:8000/api/v1/memories \
-H "X-API-Key: standalone" -H "X-Agent-ID: backend-dev" -H "Content-Type: application/json" \
-d '{"tenant_id":"default","agent_id":"backend-dev","fleet_id":"dev-fleet","scope":"fleet",
"content":"The Redis cache also stores short-lived idempotency tokens for the payments API (TTL 60s, key prefix idem:payments:)."}'
# → {"id":"2328b906-...","entity_links":[],...}
```
The write returns immediately with `entity_links: []` — entity resolution is async, so wait a few seconds. Then list entities:
```bash
curl -s "http://localhost:8000/api/v1/entities?tenant_id=default&limit=100" \
-H "X-API-Key: standalone" | jq '.[] | select(.canonical_name | contains("redis"))'
```
```json
{ "id": "07b7933e-...", "canonical_name": "redis",
"entity_type": "technology", "memory_count": 6 } // was 1
{ "id": "016336b6-...", "canonical_name": "redis connection pool",
"entity_type": "concept", "memory_count": 1 }
{ "id": "8bf57b15-...", "canonical_name": "redis_pool_size",
"entity_type": "concept", "memory_count": 3 }
```
No new "redis cache" entity appeared. The phrase resolved into the existing canonical `redis` (id `07b7933e`) — its `memory_count` is the only thing that changed. New memories with new surface forms accrue *into* the canonical entity instead of creating a forest of synonym duplicates. (You'd see this break down in the Part 2 graph view: a fleet that *didn't* resolve synonyms would have a starburst of `Redis cache` / `Redis store` / `the redis instance` orbiting nothing.)
The threshold is **0.85 cosine** between the new surface form's embedding and the existing entity's; below it, the engine creates a new canonical entity. If you ever do see a duplicate (say, an acronym that doesn't embed close to its expansion), the crystallizer's near-duplicate sweep from Part 5 picks it up on the next pass.
---
## Step 3 — Relations carry evidence
Entities alone are just a vocabulary. The recall lift comes from the **edges** between them. Pull the whole graph:
```bash
curl -s "http://localhost:8000/api/v1/graph?tenant_id=default" \
-H "X-API-Key: standalone" | jq '{nodes: (.nodes|length), edges: (.edges|length)}'
# → { "nodes": 33, "edges": 46 }
```
Look at just the edges touching `redis` (`07b7933e`):
```json
[
{ "relation_type": "uses", "source": "5d526d26", "target": "07b7933e",
"evidence_memory_id": "bb5746fc" }, // payments api uses redis
{ "relation_type": "implements", "source": "07b7933e", "target": "016336b6",
"evidence_memory_id": "bb5746fc" }, // redis implements the connection pool
{ "relation_type": "pool_is_capped_at", "source": "07b7933e", "target": "8bf57b15",
"evidence_memory_id": "e121490e" }, // … which is capped at REDIS_POOL_SIZE
{ "relation_type": "must_bump_before_merging", "source": "8bf57b15", "target": "07b7933e",
"evidence_memory_id": "e121490e" }, // and the rule that wraps the gotcha
{ "relation_type": "correlates_with", "source": "1bc69421", "target": "07b7933e",
"evidence_memory_id": "d1b871c0" }, // backend-dev's scope_agent hypothesis
{ "relation_type": "stores", "source": "07b7933e", "target": "269d4bb6",
"evidence_memory_id": "2328b906" } // the Step-2 idempotency write
]
```
Three things to notice. First, the predicates are *content-specific*, not a fixed enum — `pool_is_capped_at` and `must_bump_before_merging` came from the LLM reading the prose, not from a hand-rolled relation taxonomy. Second, every edge points back at its **evidence memory** (`evidence_memory_id`), so the graph is *queryable*: "show me the source for this assertion" is a 1-hop lookup. Third, the graph grew across writes from *different agents* — `bb5746fc` (backend-dev's decision), `e121490e` (code-reviewer's rule), `d1b871c0` (backend-dev's private hypothesis from Part 3), `2328b906` (the synonym write above). Five memories from three agents collapsed into one connected subgraph rooted at `redis`.
This is what the Part 2 dashboard's **Graph** tab is rendering. Open it now and the same edges are there — just hovered, instead of curled.
---
## Step 4 — Drill into one entity: `memclaw_entity_get`
When an agent wants everything it knows about one thing, it calls `memclaw_entity_get` (or hits the REST sibling). One call returns the entity's canonical metadata **and** every memory that mentions it:
```bash
curl -s "http://localhost:8000/api/v1/entities/07b7933e-...?tenant_id=default" \
-H "X-API-Key: standalone"
```
```json
{
"id": "07b7933e-...", "canonical_name": "redis", "entity_type": "technology",
"attributes": { "_aliases": ["redis"] },
"linked_memories": [
{ "id": "bb5746fc-...", "memory_type": "fact", "status": "conflicted",
"title": "Payments API rate limit (100 req/min) using Redis sliding window",
"weight": 0.85, "agent_id": "backend-dev" },
{ "id": "7f51244a-...", "memory_type": "fact", "status": "confirmed",
"title": "Payments API rate limit raised to 300 req/min per API key",
"weight": 0.90, "agent_id": "backend-dev" },
{ "id": "e121490e-...", "memory_type": "rule", "status": "active",
"title": "Bump REDIS_POOL_SIZE before merging Redis-backed PRs",
"weight": 0.95, "agent_id": "code-reviewer" },
{ "id": "2328b906-...", "memory_type": "fact", "status": "active",
"title": "Redis stores payments idempotency tokens (TTL 60s)",
"weight": 0.75, "agent_id": "backend-dev" },
{ "id": "2f2918e2-...", "memory_type": "insight", "status": "active",
"title": "Payments rate limit conflict: 300 vs 100 req/min",
"weight": 0.93, "agent_id": "backend-dev" },
{ "id": "d1b871c0-...", "memory_type": "insight", "status": "active",
"title": "Hypothesis: 502s correlate with Redis pool exhaustion",
"weight": 0.55, "agent_id": "backend-dev" }
]
}
```
Six memories from three agents, across five memory types (fact, rule, insight, outcome are all present in the fleet), with the full status lifecycle from Part 5 reflected (`active`, `confirmed`, `conflicted`). And **scope is honored** — `d1b871c0` is `scope_agent` (backend-dev's private hypothesis from Part 3); a non-author calling `memclaw_entity_get` on the same id wouldn't see it in the list.
This is the call to reach for when an agent needs *all context on one subject before acting* — much sharper than a string search for "redis", which would also pull in any memory that mentioned the word in passing.
---
## Step 5 — How the graph lifts recall (and how to tune it)
Hybrid search in MemClaw is three signals in one query: **semantic** (pgvector cosine over the embedding), **keyword** (BM25-style lexical), and **graph-expansion** (walk relation edges from the entities the query maps to, scoop up memories on those entities, blend them in). On the same idempotency query as Step 2:
```bash
curl -X POST http://localhost:8000/api/v1/search \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"tenant_id":"default","query":"idempotency tokens for payments","top_k":5}'
```
```json
[
{ "title": "Redis stores payments idempotency tokens (TTL 60s)", "memory_type": "fact" },
{ "title": "Payments API rate limit (100 req/min) using Redis sliding…", "memory_type": "fact" },
{ "title": "Bump REDIS_POOL_SIZE before merging Redis-backed PRs", "memory_type": "rule" },
{ "title": "Payments API rate-limit docs: 100 req/min per API key", "memory_type": "fact" },
{ "title": "Payments API rate limit raised to 300 req/min per API key", "memory_type": "fact" }
]
```
Only the first hit lexically contains "idempotency." The other four came back because the query mapped to the **payments api** and **redis** entities, and graph-expansion walked their edges — pulling in the rate-limit decision, the pool-bump rule, the docs reference, and the latest rate-limit revision. None of them mention "idempotency" anywhere, but they're all things you'd want in the same context window when reasoning about idempotency-token storage on Redis.
Graph expansion is controlled by two parameters per query (or per-agent default):
- **`graph_expand`** (bool) — whether to expand at all.
- **`graph_max_hops`** (0–3) — how far to walk. `0` = semantic + keyword only. `1` = one edge away. `2` = entity-of-entity (e.g., `idempotency-tokens` → `redis` → `redis-pool`). `3` is rarely useful and starts to over-include.
A reviewer agent that wants precision pins `graph_max_hops=0` or `1`. A docs-writer that wants breadth pins `2`. You don't have to pass it on every call — set it once on the agent via `memclaw_tune`:
```bash
curl -X PATCH "http://localhost:8000/api/v1/agents/docs-writer/tune?tenant_id=default" \
-H "X-API-Key: standalone" -H "Content-Type: application/json" \
-d '{"graph_expand": true, "graph_max_hops": 2}'
```
That single change shifts how *every* future recall by `docs-writer` ranks — same query, broader pull. Combined with the per-agent `min_similarity` and `top_k` knobs Part 4 mentioned, search becomes per-role, not one-size-fits-all.
---
## Where it pays off
The graph is the reason recall in MemClaw feels less like search and more like the fleet remembering. A reviewer agent that asks *"what should I watch out for in a Redis PR?"* doesn't get hits on the exact phrase — it gets the rate-limit decision (`bb5746fc`), the pool-bump rule (`e121490e`), and the contradiction insight (`2f2918e2`), because all three are on the **redis** node. A docs-writer asking *"what's the current state of payments rate limiting?"* gets the conflicted memory and the supersession in one call, with `graph_max_hops=2` reaching across the rate-limiter → algorithm edges. And `memclaw_entity_get` gives any agent a typed view onto one subject in one round-trip — the upgrade from "I searched and got 12 fragments" to "show me the file on this thing."
You don't write any of this. You write prose. The graph builds itself, the synonyms resolve themselves, and the recall walks edges your agents never had to name. By the time the fleet has 1,000 memories the graph has 200 nodes; at 26,500 (eToro's published number) it has thousands — and that density is exactly what lets the system answer like the corpus is in head, not on disk.
---
## Where to go from here
You've now walked the full series — fleet, dashboard, governance, the Karpathy loop, hygiene, and the graph. The pattern that ties it together: every layer rests on the layer below being **automatic**. Enrichment happens on every write. Governance is the default, not a config sprint. Outcomes feed back without ceremony. Hygiene runs on a timer. And the graph quietly accumulates — paying off, mostly invisibly, every time anyone calls `memclaw_recall`.
The MemClaw OSS repo is at **[github.com/caura-ai/caura-memclaw](https://github.com/caura-ai/caura-memclaw)** (Apache 2.0). The case study at [memclaw.net/blog/etoro-company-brain](https://memclaw.net/blog/etoro-company-brain/) is the same architecture at 300+ agents and 26,500+ memories — same engine, same primitives, same MCP surface. Build a fleet, give it a brain, let it compound.
---
**caura-memclaw · Apache 2.0** — entity extraction, resolution (≥ 0.85 cosine), typed relations with `evidence_memory_id`, `GET /api/v1/graph`, `memclaw_entity_get`, and the graph-expansion search blend are all in the open-source engine. ⭐ [Star on GitHub](https://github.com/caura-ai/caura-memclaw) · [Join Discord](https://discord.com/invite/aNfpgfpj) · [memclaw.net](https://memclaw.net)
A fleet's memory is only as good as what it can find. The graph is what makes finding feel like remembering.
# The Memory Dashboard: a browsable window into your fleet's mind
URL: https://memclaw.net/docs/tutorials/memory-dashboard
Description: See, search, and govern your fleet's shared memory — from one HTML file and a reverse proxy.
In [Part 1](/docs/tutorials/multi-agent-fleet) we gave three Claude Code agents one shared, governed memory. They write decisions, recall each other's gotchas, and compound knowledge across sessions. It works.
But there's a problem you hit about ten minutes in: **you can't see any of it.** The memory is doing its job inside Postgres and a vector index, and your only window is `curl`. You can't answer simple questions — *What does the fleet know? Who wrote that? Why does the docs agent believe X? What got superseded?* — without hand-writing API calls.
So in this part we build a **memory dashboard**: a single-page app that browses every memory, runs semantic search, visualizes the knowledge graph, shows the audit trail, and lets you write and govern memories by hand. No framework, no build step — one `index.html` and a tiny `nginx` config, added to the same Docker stack from Part 1.
> Everything here talks to the standard MemClaw **REST API** (`/api/v1/...`). If you're on the managed platform or a different client, only the base URL and key change.
---
## What we're building
A two-pane app served at `http://localhost:8090`:
- **Browse & semantic search** — every memory as a card, or paraphrase a query and get ranked hits with similarity scores.
- **Sidebar** — live stats (total, by type, status, agent — click to filter) so you can slice the corpus instantly.
- **Write** — a form that creates a memory (MemClaw enriches and dedups it).
- **Govern** — per-memory lifecycle transitions and soft-delete.
- **Graph** — a force-directed view of the entities and relations MemClaw extracted.
- **Audit** — the provenance chain of every write, transition, and delete.
We build it in small snippets across Steps 3–9, then assemble them into one `ui/index.html` in Step 10. No framework, no build step.
---
## The one decision that matters: don't fight CORS, proxy around it
A browser app calling `http://localhost:8000/api/v1/...` from a page served somewhere else is a **cross-origin** request. You'd need MemClaw's `CORS_ORIGINS` to list your UI's origin, and the custom `X-API-Key` header triggers a preflight `OPTIONS` on every call. It's fiddly, and it leaks your API key into browser-visible JavaScript.
The clean fix: **serve the SPA and the API from the same origin.** Put an `nginx` in front that serves the static file *and* reverse-proxies `/api/` to `core-api`. The browser only ever talks to nginx — no CORS, ever — and nginx injects the API key on the way through, so the key never touches the client.
```
┌─────────────┐ same origin (http://localhost:8090)
│ Browser │
│ index.html │──┐
└─────────────┘ │ GET / → static index.html
│ GET /api/v1/memories │
▼ ▼ proxy_pass + X-API-Key
┌──────────────┐ ┌──────────────────┐
│ nginx │────────▶│ core-api │
│ (ui sidecar)│ │ (MemClaw REST) │
└──────────────┘ └──────────────────┘
```
---
## Step 1 — the nginx config
`ui/nginx.conf`:
```nginx
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Static SPA — fall back to index.html for client routing.
location / {
try_files $uri $uri/ /index.html;
}
# MemClaw REST API. proxy_pass with no path preserves the full /api/... URI.
# The standalone key is injected here, so the browser never handles auth.
location /api/ {
proxy_pass http://core-api:8000;
proxy_set_header Host $host;
proxy_set_header X-API-Key standalone;
proxy_read_timeout 60s;
}
}
```
Two things to note: `proxy_pass http://core-api:8000` *without* a trailing path preserves the incoming URI, so `/api/v1/search` lands at `core-api:8000/api/v1/search`. And `core-api` resolves by service name because the sidecar shares the compose network.
## Step 2 — add the sidecar to the stack
In `docker-compose.yml`:
```yaml
ui:
image: nginx:1.27-alpine
ports:
- "${UI_PORT:-8090}:80"
volumes:
- ./ui/index.html:/usr/share/nginx/html/index.html:ro
- ./ui/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
core-api:
condition: service_healthy
```
```bash
# Use the SAME compose project as Part 1 (the default, named after the repo dir)
# so the sidecar attaches to the core-api you already have running.
docker compose up -d ui
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8090/ # 200
curl -s http://localhost:8090/api/v1/memories/stats
# {"total":4,"by_type":{"decision":1,"fact":1,"insight":1,"outcome":1},...}
```
Because `index.html` is bind-mounted, editing it updates the live app on refresh — no rebuild while you iterate.
---
## Step 3 — the REST surface, and two tiny helpers
The whole app is built from nine endpoints. Worth knowing them even if you never build a UI:
| What | Call |
|---|---|
| Stats | `GET /api/v1/memories/stats` → `{total, by_type, by_agent, by_status}` |
| Browse | `GET /api/v1/memories?tenant_id=default&limit=200` → `{items:[…], next_cursor}` |
| Semantic search | `POST /api/v1/search` `{tenant_id, query, top_k}` → `{items:[…]}` (each with `similarity`) |
| Graph | `GET /api/v1/graph?tenant_id=default` → `{nodes, edges}` |
| Audit | `GET /api/v1/audit-log` |
| Write | `POST /api/v1/memories` `{tenant_id, agent_id, content, …}` |
| Transition | `PATCH /api/v1/memories/{id}/status?tenant_id=default` `{status}` |
| Delete | `DELETE /api/v1/memories/{id}?tenant_id=default` (soft) |
Two helpers carry the whole app. `api()` wraps `fetch` and surfaces MemClaw's error shape; `el()` builds a DOM node and sets its text via **`textContent`** — which is the important bit: memory content is **attacker-controlled** (any agent can write ``), and `textContent` renders it as inert text, so building the UI with `el()` instead of `innerHTML` strings means **no stored-XSS hole** anywhere a memory is shown.
```js
const API = '/api/v1';
const TENANT = 'default';
const STATUSES = ['active','pending','confirmed','cancelled','outdated','conflicted','archived','deleted'];
let activeFilter = { type:null, status:null, agent:null };
let allMemories = [];
const $ = s => document.querySelector(s);
const el = (tag, cls, txt) => { const e=document.createElement(tag); if(cls)e.className=cls; if(txt!=null)e.textContent=txt; return e; };
function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2200); }
async function api(path, opts){
const r = await fetch(API + path, opts);
if(!r.ok){ let d=''; try{ d=JSON.stringify(await r.json()); }catch{} throw new Error('HTTP '+r.status+' '+d); }
return r.status===204 ? null : r.json();
}
```
## Step 4 — browse, search, and render the cards
Browse is a plain list; search is the same render path with a `similarity` badge. The only rule to remember: **REST `/search` caps `top_k` at 20** (it returns `422` above that — the MCP `memclaw_recall` tool has no such cap).
`renderCards()` builds each card with `el()` — type badge (color-coded), title, content, author, weight, an 8-state status dropdown, and a delete button. Every field goes through `textContent`, so nothing a memory contains can inject markup.
```js
function badgeClass(t){ return 'badge b-'+(['fact','decision','insight','outcome','semantic'].includes(t)?t:'default'); }
async function browse(){
const d = await api(`/memories?tenant_id=${TENANT}&limit=200`);
allMemories = d.items || []; renderCards();
}
async function search(){
const q = $('#q').value.trim(); if(!q) return browse();
const top_k = Math.min(20, Math.max(1, parseInt($('#topk').value)||10));
const d = await api('/search', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ tenant_id:TENANT, query:q, top_k }) });
allMemories = d.items || []; renderCards(true);
}
function renderCards(showSim){
const box = $('#cards'); box.innerHTML='';
const list = allMemories.filter(m =>
(!activeFilter.type || m.memory_type===activeFilter.type) &&
(!activeFilter.status || m.status===activeFilter.status) &&
(!activeFilter.agent || m.agent_id===activeFilter.agent));
if(!list.length){ box.appendChild(el('div','empty','No memories match.')); return; }
list.forEach(m => {
const c = el('div','card');
const row = el('div','row');
row.appendChild(el('span', badgeClass(m.memory_type), m.memory_type||'memory'));
row.appendChild(el('span', 'vis '+(m.visibility||''), (m.visibility||'').replace('scope_','👁 ')));
if(showSim && m.similarity!=null) row.appendChild(el('span','sim','sim '+m.similarity.toFixed(3)));
c.appendChild(row);
if(m.title) c.appendChild(el('div','title', m.title));
c.appendChild(el('div','content', (m.content||'').slice(0,240)));
const meta = el('div','meta');
meta.appendChild(el('span', null, '👤 '+(m.agent_id||'?')));
meta.appendChild(el('span', null, '⭑ '+(m.weight??'–')));
c.appendChild(meta);
const act = el('div','actions');
const sel = el('select'); STATUSES.forEach(s=>{ const o=el('option',null,s); if(s===m.status)o.selected=true; sel.appendChild(o); });
sel.onchange = async () => { try{ await api(`/memories/${m.id}/status?tenant_id=${TENANT}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({status:sel.value})}); toast('status → '+sel.value); m.status=sel.value; loadStats(); }catch(e){ toast(e.message); } };
act.appendChild(sel);
const del = el('button','del','Delete');
del.onclick = async () => { if(!confirm('Soft-delete this memory?'))return; try{ await api(`/memories/${m.id}?tenant_id=${TENANT}`,{method:'DELETE'}); toast('deleted'); browse(); loadStats(); }catch(e){ toast(e.message); } };
act.appendChild(del);
c.appendChild(act);
box.appendChild(c);
});
}
```
Search is the part that sells the whole platform. Type *"redis connection pool limits"* and the top hit is a memory whose text never says "limits" — that's the hybrid retrieval (vector + keyword + graph) doing its job; the dashboard just shows the `similarity` it returns.
## Step 5 — the sidebar: stats and click-to-filter
The sidebar reads `/memories/stats` and turns `by_type` / `by_status` / `by_agent` into clickable chips. Filtering is client-side against the already-loaded set, so toggling a chip is instant:
```js
async function loadStats(){
const s = await api(`/memories/stats?tenant_id=${TENANT}`);
$('#statTotal').textContent = s.total ?? 0;
renderChips('#byType', s.by_type, 'type');
renderChips('#byStatus', s.by_status, 'status');
renderChips('#byAgent', s.by_agent, 'agent');
}
function renderChips(sel, obj, key){
const box = $(sel); box.innerHTML='';
Object.entries(obj||{}).sort((a,b)=>b[1]-a[1]).forEach(([k,v]) => {
const c = el('span', 'chip'+(activeFilter[key]===k?' on':''));
c.appendChild(el('span', null, k+' ')); // el() => textContent => safe even though k can be an agent_id
c.appendChild(el('b', null, String(v)));
c.onclick = () => { activeFilter[key] = activeFilter[key]===k?null:k; renderCards(); loadStats(); };
box.appendChild(c);
});
}
```
(Heads-up: agents auto-register on first *write*, so an agent that has only ever *recalled* won't appear in `by_agent` yet.)
## Step 6 — write a memory
The form posts to `/memories`. Two things the API teaches you immediately:
- **`agent_id` is required** — omit it and you get `422`. Every memory is authored by an identity; that's what makes governance possible.
- **Writes are deduplicated.** Submit something semantically identical to an existing memory and the engine declines it — treat that as success-ish in the UI, not a crash.
```js
async function writeMemory(){
const agent_id = $('#wAgent').value.trim(), content = $('#wContent').value.trim(), visibility = $('#wVis').value;
if(!content) return toast('content required');
try{
await api('/memories', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ tenant_id:TENANT, agent_id, content, fleet_id:'dev-fleet', visibility }) });
$('#wContent').value=''; toast('memory written'); setTimeout(()=>{ browse(); loadStats(); }, 400);
}catch(e){ toast(/dup/i.test(e.message)?'Duplicate — MemClaw deduped it (not written)':e.message); }
}
```
You send raw text and a `visibility` (`scope_team` by default); MemClaw returns it enriched with an inferred `memory_type`, `title`, `summary`, `tags`, and extracted entities.
## Step 7 — govern: transitions and soft-delete
You already wired these into each card in Step 4 — the status `