mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 07:42:43 +00:00
Co-authored-by: Dhravya <63950637+Dhravya@users.noreply.github.com> Co-authored-by: docs <docs@supermemory.ai>
337 lines
9.1 KiB
Text
337 lines
9.1 KiB
Text
---
|
|
title: "Cloudflare"
|
|
description: "Give your AI agent persistent memory inside a Cloudflare Container using SMFS"
|
|
---
|
|
|
|
Mount a Supermemory container inside a
|
|
[Cloudflare Container](https://developers.cloudflare.com/containers/) so your
|
|
agent can read and write memory using standard filesystem commands.
|
|
|
|
## How it works
|
|
|
|
There are two ways to wire SMFS into a Cloudflare Container — pick the one that
|
|
fits your architecture.
|
|
|
|
### Agent inside the container
|
|
|
|
The agent process runs inside the container with direct access to the SMFS
|
|
mount. The entrypoint sets up the mount and starts the agent.
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Cloudflare Container
|
|
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["/memory<br/>(SMFS mount)"]
|
|
end
|
|
Mount -->|sync| SM["Supermemory"]
|
|
```
|
|
|
|
### Agent outside the container
|
|
|
|
The agent runs in a Cloudflare Worker and sends commands to the container over
|
|
HTTP. The container exposes a simple exec endpoint.
|
|
|
|
```mermaid
|
|
graph LR
|
|
Agent["Worker<br/>(agent logic)"] -->|"containerFetch('/exec')"| Container
|
|
subgraph Container ["Cloudflare Container"]
|
|
Mount["/memory<br/>(SMFS mount)"]
|
|
end
|
|
Mount -->|sync| SM["Supermemory"]
|
|
```
|
|
|
|
## Prerequisites
|
|
|
|
- A [Supermemory API key](https://supermemory.ai)
|
|
- An [Anthropic API key](https://console.anthropic.com)
|
|
- A [Cloudflare account](https://dash.cloudflare.com) with Containers enabled (Workers Paid plan)
|
|
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
|
|
- The [`@cloudflare/containers`](https://www.npmjs.com/package/@cloudflare/containers) package: `npm install @cloudflare/containers`
|
|
|
|
<Note>
|
|
Cloudflare Containers are implemented as container-enabled Durable Objects.
|
|
You declare a `Container` subclass, bind it as a Durable Object, and
|
|
reference its image in the `containers` array. Worker secrets are **not**
|
|
automatically visible inside the container — you have to pass them through
|
|
`envVars` when starting the container (see below).
|
|
</Note>
|
|
|
|
---
|
|
|
|
## Pattern A: Agent inside the container
|
|
|
|
SMFS and the Claude Agent SDK are baked into the container image. On startup,
|
|
the entrypoint mounts memory and runs the agent.
|
|
|
|
### Dockerfile
|
|
|
|
```dockerfile Dockerfile
|
|
FROM python:3.12-slim
|
|
|
|
RUN apt-get update && apt-get install -y fuse3 curl bash && rm -rf /var/lib/apt/lists/*
|
|
RUN echo 'user_allow_other' >> /etc/fuse.conf
|
|
|
|
RUN curl -fsSL https://smfs.ai/install | bash -s -- 0.0.1-rc2
|
|
ENV PATH="/root/.local/bin:$PATH"
|
|
RUN pip install claude-agent-sdk
|
|
|
|
COPY agent.py /app/agent.py
|
|
COPY entrypoint.sh /entrypoint.sh
|
|
RUN chmod +x /entrypoint.sh
|
|
|
|
ENTRYPOINT ["/entrypoint.sh"]
|
|
```
|
|
|
|
### Entrypoint
|
|
|
|
```bash entrypoint.sh
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
smfs login --key "$SUPERMEMORY_API_KEY"
|
|
smfs mount my_agent --ephemeral --path /memory --foreground &
|
|
sleep 3
|
|
|
|
exec python3 /app/agent.py
|
|
```
|
|
|
|
### Agent code
|
|
|
|
```python agent.py
|
|
import asyncio
|
|
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
|
|
MEMORY = "/memory"
|
|
|
|
async def main():
|
|
async for message in query(
|
|
prompt=f"You have a persistent memory filesystem at {MEMORY}. "
|
|
"Read profile.md to learn about the user, then create "
|
|
"session_notes.md summarizing what you found.",
|
|
options=ClaudeAgentOptions(
|
|
allowed_tools=["Bash", "Read", "Write"],
|
|
cwd=MEMORY,
|
|
),
|
|
):
|
|
print(message)
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
### Worker
|
|
|
|
The Worker defines the `Container` subclass and forwards Worker secrets into
|
|
the container via `envVars`:
|
|
|
|
```typescript worker.ts
|
|
import { Container, getContainer } from "@cloudflare/containers";
|
|
|
|
export class MyAgentContainer extends Container {
|
|
defaultPort = 8080;
|
|
// Forward Worker secrets into the container at start time.
|
|
// `this.env` is the Worker env object, populated from wrangler secrets.
|
|
envVars = {
|
|
SUPERMEMORY_API_KEY: this.env.SUPERMEMORY_API_KEY,
|
|
ANTHROPIC_API_KEY: this.env.ANTHROPIC_API_KEY,
|
|
};
|
|
}
|
|
|
|
export default {
|
|
async fetch(request: Request, env: Env) {
|
|
// The container runs the agent and exits; this Worker route just kicks
|
|
// it off (e.g. on a queue message or scheduled trigger).
|
|
const container = getContainer(env.MY_CONTAINER, "agent-singleton");
|
|
return container.fetch(request);
|
|
},
|
|
};
|
|
|
|
interface Env {
|
|
MY_CONTAINER: DurableObjectNamespace<MyAgentContainer>;
|
|
SUPERMEMORY_API_KEY: string;
|
|
ANTHROPIC_API_KEY: string;
|
|
}
|
|
```
|
|
|
|
### Config
|
|
|
|
```jsonc wrangler.jsonc
|
|
{
|
|
"name": "memory-agent",
|
|
"main": "worker.ts",
|
|
"compatibility_date": "2025-04-03",
|
|
"containers": [
|
|
{
|
|
"class_name": "MyAgentContainer",
|
|
"image": "./Dockerfile",
|
|
"max_instances": 5
|
|
}
|
|
],
|
|
"durable_objects": {
|
|
"bindings": [
|
|
{ "name": "MY_CONTAINER", "class_name": "MyAgentContainer" }
|
|
]
|
|
},
|
|
"migrations": [
|
|
{ "tag": "v1", "new_sqlite_classes": ["MyAgentContainer"] }
|
|
]
|
|
}
|
|
```
|
|
|
|
```bash
|
|
wrangler secret put SUPERMEMORY_API_KEY
|
|
wrangler secret put ANTHROPIC_API_KEY
|
|
wrangler deploy
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern B: Agent outside the container
|
|
|
|
The agent logic lives in a Worker. The container just runs SMFS and exposes an
|
|
HTTP endpoint for executing commands against the mount.
|
|
|
|
<Warning>
|
|
The `/exec` endpoint below runs arbitrary shell commands inside the
|
|
container. **Only call it from your Worker** — never expose it publicly,
|
|
and never pass user input straight into `command` without validation.
|
|
Cloudflare Containers are addressable only through their Worker by default,
|
|
so this is safe as long as you don't add a public route that proxies to
|
|
`/exec`.
|
|
</Warning>
|
|
|
|
### Container (exec server)
|
|
|
|
The Dockerfile and entrypoint are nearly identical to Pattern A — the only
|
|
differences are the Python deps (`flask` instead of `claude-agent-sdk`) and
|
|
the file we exec at the end.
|
|
|
|
```dockerfile Dockerfile
|
|
FROM python:3.12-slim
|
|
|
|
RUN apt-get update && apt-get install -y fuse3 curl bash && rm -rf /var/lib/apt/lists/*
|
|
RUN echo 'user_allow_other' >> /etc/fuse.conf
|
|
|
|
RUN curl -fsSL https://smfs.ai/install | bash -s -- 0.0.1-rc2
|
|
ENV PATH="/root/.local/bin:$PATH"
|
|
RUN pip install flask gunicorn
|
|
|
|
COPY server.py /app/server.py
|
|
COPY entrypoint.sh /entrypoint.sh
|
|
RUN chmod +x /entrypoint.sh
|
|
|
|
ENTRYPOINT ["/entrypoint.sh"]
|
|
```
|
|
|
|
The entrypoint differs from Pattern A only in the final `exec` line — we run
|
|
gunicorn against the Flask app instead of `python3 agent.py`:
|
|
|
|
```bash entrypoint.sh
|
|
#!/bin/bash
|
|
set -e
|
|
|
|
smfs login --key "$SUPERMEMORY_API_KEY"
|
|
smfs mount my_agent --ephemeral --path /memory --foreground &
|
|
sleep 3
|
|
|
|
exec gunicorn -b 0.0.0.0:8080 --chdir /app server:app
|
|
```
|
|
|
|
```python server.py
|
|
import subprocess
|
|
from flask import Flask, request, jsonify
|
|
|
|
app = Flask(__name__)
|
|
|
|
@app.route("/exec", methods=["POST"])
|
|
def exec_command():
|
|
cmd = request.json["command"]
|
|
result = subprocess.run(
|
|
cmd, shell=True, capture_output=True, text=True, cwd="/memory", timeout=10
|
|
)
|
|
return jsonify(stdout=result.stdout, stderr=result.stderr, code=result.returncode)
|
|
```
|
|
|
|
<Note>
|
|
We use gunicorn instead of `app.run(...)` because Flask's built-in dev
|
|
server isn't meant for production traffic. If you'd rather just see it
|
|
work, you can replace the `exec` line with
|
|
`exec python3 /app/server.py` and add `app.run(host="0.0.0.0", port=8080)`
|
|
to `server.py` — but switch back to gunicorn before you ship.
|
|
</Note>
|
|
|
|
### Worker (agent logic)
|
|
|
|
```typescript worker.ts
|
|
import { Container, getContainer } from "@cloudflare/containers";
|
|
|
|
export class ExecContainer extends Container {
|
|
defaultPort = 8080;
|
|
envVars = {
|
|
SUPERMEMORY_API_KEY: this.env.SUPERMEMORY_API_KEY,
|
|
};
|
|
}
|
|
|
|
export default {
|
|
async fetch(_request: Request, env: Env) {
|
|
const container = getContainer(env.MY_CONTAINER, "agent-singleton");
|
|
|
|
const profile = await container
|
|
.fetch(new Request("http://container/exec", {
|
|
method: "POST",
|
|
body: JSON.stringify({ command: "cat /memory/profile.md" }),
|
|
headers: { "Content-Type": "application/json" },
|
|
}))
|
|
.then((r) => r.json<{ stdout: string }>());
|
|
|
|
return Response.json({ profile: profile.stdout });
|
|
},
|
|
};
|
|
|
|
interface Env {
|
|
MY_CONTAINER: DurableObjectNamespace<ExecContainer>;
|
|
SUPERMEMORY_API_KEY: string;
|
|
}
|
|
```
|
|
|
|
### Config
|
|
|
|
```jsonc wrangler.jsonc
|
|
{
|
|
"name": "memory-exec",
|
|
"main": "worker.ts",
|
|
"compatibility_date": "2025-04-03",
|
|
"containers": [
|
|
{
|
|
"class_name": "ExecContainer",
|
|
"image": "./Dockerfile",
|
|
"max_instances": 5
|
|
}
|
|
],
|
|
"durable_objects": {
|
|
"bindings": [
|
|
{ "name": "MY_CONTAINER", "class_name": "ExecContainer" }
|
|
]
|
|
},
|
|
"migrations": [
|
|
{ "tag": "v1", "new_sqlite_classes": ["ExecContainer"] }
|
|
]
|
|
}
|
|
```
|
|
|
|
```bash
|
|
wrangler secret put SUPERMEMORY_API_KEY
|
|
wrangler deploy
|
|
```
|
|
|
|
---
|
|
|
|
## Tips
|
|
|
|
- Use `--ephemeral` for container mounts — keeps the cache in memory only, but
|
|
writes still push to Supermemory
|
|
- Use `smfs grep 'query'` for semantic search across all files
|
|
- Worker secrets aren't automatically visible inside the container. Pass each
|
|
one through the `envVars` field on your `Container` subclass (see the Worker
|
|
snippets above)
|
|
- Use `containerFetch` from within a Container class method (e.g., lifecycle
|
|
hooks) to call the container's own HTTP server. From the Worker, use the
|
|
stub's `.fetch()` method instead
|