Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ External Services relayfile Your Agents
4. **Agents read and write files** — that's their entire integration
5. When an agent writes to a writeback path, the adapter posts the change back to the source API

## Quick Start (Docker)

```bash
cd docker && docker compose up --build
```

This starts relayfile on `:9090` and relayauth on `:9091`, seeds a `ws_demo` workspace with sample files, and prints a dev token. See [`docker/README.md`](docker/README.md) for details.

## Getting Started

**[Relayfile Cloud](https://relayfile.dev/pricing)** — everything managed. Sign up, get a token, connect your services from the dashboard.
Expand Down
5 changes: 5 additions & 0 deletions docker/Dockerfile.relayauth
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM node:20-alpine
WORKDIR /app
COPY relayauth/server.js .
EXPOSE 9091
CMD ["node", "server.js"]
12 changes: 12 additions & 0 deletions docker/Dockerfile.relayfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/relayfile ./cmd/relayfile

FROM alpine:3.20
RUN apk add --no-cache ca-certificates curl
COPY --from=build /out/relayfile /usr/local/bin/relayfile
EXPOSE 8080
CMD ["/usr/local/bin/relayfile"]
48 changes: 48 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Docker Quickstart

Run relayfile + relayauth locally in under a minute.

## Start

```bash
cd docker
docker compose up --build
```

This starts:

| Service | URL | Description |
|---------|-----|-------------|
| relayfile | http://localhost:9090 | VFS API server (Go, in-memory) |
| relayauth | http://localhost:9091 | Token signing service (Node) |
| seed | (runs once) | Creates `ws_demo` workspace with sample files |

The `seed` container mints a dev token and writes three sample files. Watch its logs for the token and a ready-to-paste `curl` command.

## Usage

```bash
# Grab the token from seed logs
TOKEN=$(docker compose logs seed | grep 'token' | tail -1 | awk '{print $NF}')

# List files
curl -H "Authorization: Bearer $TOKEN" http://localhost:9090/v1/workspaces/ws_demo/fs/tree

# Read a file
curl -H "Authorization: Bearer $TOKEN" "http://localhost:9090/v1/workspaces/ws_demo/fs/file?path=/docs/welcome.md"

# Mint a new token
curl -X POST http://localhost:9091/sign \
-H "Content-Type: application/json" \
-d '{"workspace_id":"ws_demo","agent_name":"my-agent","scopes":["fs:read"]}'
```

## Configuration

Copy `.env.example` to `.env` to override defaults. The in-memory backend means data resets on restart — perfect for development.

## Teardown

```bash
docker compose down
```
42 changes: 42 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
services:
relayfile:
build:
context: ..
dockerfile: docker/Dockerfile.relayfile
ports:
- "${RELAYFILE_PORT:-9090}:8080"
environment:
RELAYFILE_ADDR: :8080
RELAYFILE_BACKEND_PROFILE: memory
RELAYFILE_JWT_SECRET: ${RELAYFILE_JWT_SECRET:-dev-secret}
RELAYFILE_INTERNAL_HMAC_SECRET: ${RELAYFILE_INTERNAL_HMAC_SECRET:-dev-internal-secret}
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 3s
timeout: 3s
retries: 10

relayauth:
build:
context: .
dockerfile: Dockerfile.relayauth
ports:
- "${RELAYAUTH_PORT:-9091}:9091"
environment:
RELAYFILE_JWT_SECRET: ${RELAYFILE_JWT_SECRET:-dev-secret}
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:9091/health"]
interval: 3s
timeout: 3s
retries: 10

seed:
image: curlimages/curl:latest
depends_on:
relayfile:
condition: service_healthy
relayauth:
condition: service_healthy
volumes:
- ./seed.sh:/seed.sh:ro
entrypoint: ["/bin/sh", "/seed.sh"]
62 changes: 62 additions & 0 deletions docker/relayauth/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const http = require("node:http");
const crypto = require("node:crypto");

const PORT = 9091;
const SECRET = process.env.RELAYFILE_JWT_SECRET || "dev-secret";

function b64url(buf) {
return Buffer.from(buf)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}

function sign(payload) {
const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const body = b64url(JSON.stringify(payload));
const sig = b64url(
crypto.createHmac("sha256", SECRET).update(`${header}.${body}`).digest()
);
return `${header}.${body}.${sig}`;
}

const server = http.createServer((req, res) => {
if (req.method === "GET" && req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end('{"status":"ok"}');
}

if (req.method === "POST" && req.url === "/sign") {
let data = "";
req.on("data", (c) => (data += c));
req.on("end", () => {
try {
const { workspace_id, agent_name, scopes } = JSON.parse(data);
const token = sign({
workspace_id: workspace_id || "ws_demo",
agent_name: agent_name || "dev-agent",
scopes: scopes || [
"fs:read",
"fs:write",
"sync:read",
"ops:read",
],
exp: 4102444800,
aud: "relayfile",
});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ token }));
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end('{"error":"invalid json body"}');
}
});
return;
}

res.writeHead(404);
res.end("Not found");
});

server.listen(PORT, () => console.log(`relayauth listening on :${PORT}`));
44 changes: 44 additions & 0 deletions docker/seed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/sh
set -e

RELAYFILE=http://relayfile:8080
RELAYAUTH=http://relayauth:9091
WS=ws_demo

echo "==> Minting dev token via relayauth..."
TOKEN=$(curl -sS "$RELAYAUTH/sign" \
-H "Content-Type: application/json" \
-d "{\"workspace_id\":\"$WS\",\"agent_name\":\"dev-agent\",\"scopes\":[\"relayfile:fs:read:*\",\"relayfile:fs:write:*\",\"relayfile:sync:read:*\",\"relayfile:ops:read:*\"]}" \
| sed 's/.*"token":"\([^"]*\)".*/\1/')

echo "==> Seeding sample files into workspace $WS..."

curl -sS -X PUT "$RELAYFILE/v1/workspaces/$WS/fs/file?path=/docs/welcome.md" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "X-Correlation-Id: seed-$(date +%s)-1" \
-d '# Welcome
Your relayfile workspace is running.
Write files here and agents will see them instantly.'

curl -sS -X PUT "$RELAYFILE/v1/workspaces/$WS/fs/file?path=/github/repos/demo/pulls/1/metadata.json" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "X-Correlation-Id: seed-$(date +%s)-2" \
-d '{"number":1,"title":"Add quickstart","state":"open","author":"dev"}'

curl -sS -X PUT "$RELAYFILE/v1/workspaces/$WS/fs/file?path=/config/agents.json" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "X-Correlation-Id: seed-$(date +%s)-3" \
-d '{"agents":[{"name":"dev-agent","scopes":["relayfile:fs:read:*","relayfile:fs:write:*"]}]}'

echo ""
echo "=== Ready ==="
echo " relayfile : http://localhost:${RELAYFILE_PORT:-9090}"
echo " relayauth : http://localhost:${RELAYAUTH_PORT:-9091}"
echo " workspace : $WS"
echo " token : $TOKEN"
echo ""
echo "Try it:"
echo " curl -H \"Authorization: Bearer $TOKEN\" http://localhost:${RELAYFILE_PORT:-9090}/v1/workspaces/$WS/fs/tree"
59 changes: 59 additions & 0 deletions examples/01-agent-reads-files/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 01 — Agent reads files

The simplest relayfile example: connect to a workspace and read its contents.

## What it shows

| Method | Purpose |
|--------|---------|
| `listTree` | Browse the virtual filesystem tree |
| `readFile` | Fetch a single file's content and metadata |
| `queryFiles` | Search files by provider or semantic properties |

## Prerequisites

- Docker Engine or Docker Desktop with the Compose plugin
- Node.js 18+, `tsx`

Start the local stack from the repo root before running this example:

```bash
cd docker
docker compose up --build
```

This boots the relayfile API, relayauth, and the seeded `ws_demo`
workspace used throughout the examples.

## Run

```bash
export RELAYFILE_TOKEN="ey…" # JWT with fs:read scope
export WORKSPACE_ID="ws_demo"

npx tsx index.ts
```

## Expected output

```
── listTree (depth 2) ──
📁 /github
📄 /github/pulls/42.json rev=r_abc123

── readFile /github/pulls/42.json ──
contentType : application/json
revision : r_abc123
provider : github
content : {"title":"Add auth middleware",…}…

── queryFiles (provider=github) ──
/github/pulls/42.json props={"status":"open"}

── queryFiles (property status=open) ──
matched 1 file(s)
/github/pulls/42.json

Done.
```
78 changes: 78 additions & 0 deletions examples/01-agent-reads-files/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Example 01 — Agent reads files from a workspace
*
* Demonstrates the simplest relayfile flow: connect to a workspace and
* browse its virtual filesystem using listTree, readFile, and queryFiles.
*
* Run: npx tsx index.ts
* Env: RELAYFILE_TOKEN — JWT with fs:read scope
* WORKSPACE_ID — target workspace ID
*/

import { RelayFileClient } from "@relayfile/sdk";

const token = process.env.RELAYFILE_TOKEN;
const workspaceId = process.env.WORKSPACE_ID ?? "ws_demo";

if (!token) {
console.error("Set RELAYFILE_TOKEN before running this example.");
process.exit(1);
}

const client = new RelayFileClient({ token });

// ── 1. List the workspace tree ──────────────────────────────────────────────

console.log("── listTree (depth 2) ──");
const tree = await client.listTree(workspaceId, { depth: 2 });

for (const entry of tree.entries) {
const icon = entry.type === "dir" ? "📁" : "📄";
console.log(` ${icon} ${entry.path} rev=${entry.revision}`);
}

if (tree.nextCursor) {
console.log(` … more entries (cursor: ${tree.nextCursor})`);
}

// ── 2. Read a single file ───────────────────────────────────────────────────

const targetPath = tree.entries.find((e) => e.type === "file")?.path;

if (targetPath) {
console.log(`\n── readFile ${targetPath} ──`);
const file = await client.readFile(workspaceId, targetPath);
console.log(` contentType : ${file.contentType}`);
console.log(` revision : ${file.revision}`);
console.log(` provider : ${file.provider ?? "(agent-written)"}`);
console.log(` content : ${file.content.slice(0, 200)}…`);
} else {
console.log("\nNo files found in tree — skipping readFile.");
}

// ── 3. Query files by provider ──────────────────────────────────────────────

console.log("\n── queryFiles (provider=github) ──");
const query = await client.queryFiles(workspaceId, { provider: "github" });

if (query.items.length === 0) {
console.log(" (no github files — try ingesting a webhook first)");
} else {
for (const item of query.items.slice(0, 5)) {
console.log(` ${item.path} props=${JSON.stringify(item.properties)}`);
}
}

// ── 4. Query files by semantic property ─────────────────────────────────────

console.log("\n── queryFiles (property status=open) ──");
const byProp = await client.queryFiles(workspaceId, {
properties: { status: "open" },
});

console.log(` matched ${byProp.items.length} file(s)`);
for (const item of byProp.items.slice(0, 3)) {
console.log(` ${item.path}`);
}

console.log("\nDone.");
Loading
Loading