Skip to content
Open
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
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ If you change the payload contract, update the code, docs, examples, and the Ope
### Diff handling
- `src/lib/diff/git-patch.ts` - patch parsing support for diff rendering

### Self-hosted mode (optional add-on)
- `selfhosted/src/server.ts` - Express server with API routes and viewer route
- `selfhosted/src/db.ts` - SQLite database setup and queries
- `selfhosted/src/cleanup.ts` - Standalone expired artifact cleanup script
- `selfhosted/Dockerfile` - Multi-stage Docker build
- `selfhosted/docker-compose.yml` - Docker Compose configuration
- `src/lib/payload/injected.ts` - Injected envelope resolver for the viewer shell

### Docs and external contract
- `README.md`
- `docs/architecture.md`
Expand All @@ -136,6 +144,7 @@ If you change the payload contract, update the code, docs, examples, and the Ope
- `docs/dependency-notes.md`
- `docs/testing.md`
- `skills/agent-render-linking/SKILL.md`
- `skills/selfhosted-agent-render/SKILL.md`

## Development commands

Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,25 @@ Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based shari
## Principles

- Fully static export with Next.js App Router
- No backend, no database, no server-side persistence
- No backend, no database, no server-side persistence for the default fragment-based mode
- Fragment-based payloads (`#...`) so the server never receives artifact contents
- Public-safe naming and MIT-compatible dependencies

## Self-Hosted Mode (Optional)

An optional self-hosted variant is available in `selfhosted/` for use cases where fragment-based links are impractical (payloads too large, chat platforms mangle URLs, or persistent short links are needed).

The self-hosted server:
- Stores artifact payloads in SQLite under UUID v4 keys
- Serves the same viewer UI at `/{uuid}` routes
- Provides a simple CRUD API at `/api/artifacts`
- Implements 24-hour sliding TTL (each view extends expiry)
- Supports Docker Compose and daemon/service deployments

This is a separate add-on for power users and agents. The default static fragment-based product is unaffected.

See `docs/deployment.md` for setup instructions and `skills/selfhosted-agent-render/SKILL.md` for agent workflow guidance.

## Local Development

```bash
Expand Down Expand Up @@ -79,9 +94,10 @@ The shell keeps first load lean and defers renderer-heavy code until needed. The

- `docs/architecture.md` - architecture and tradeoffs
- `docs/payload-format.md` - fragment protocol, limits, and examples
- `docs/deployment.md` - deployment notes
- `docs/deployment.md` - deployment notes (including self-hosted mode)
- `docs/dependency-notes.md` - major dependency and license notes
- `docs/testing.md` - test commands, screenshot workflow, and CI notes
- `skills/selfhosted-agent-render/SKILL.md` - self-hosted agent workflow skill

## Zero Retention

Expand Down
41 changes: 41 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,44 @@ The static host does not receive fragment contents as part of the request, but t
- GitHub Pages-compatible `basePath` and `assetPrefix`
- `.nojekyll` included for Pages compatibility
- Fragment size budget enforced before render

## Self-hosted mode (optional)

An optional self-hosted variant lives in `selfhosted/` and provides server-backed artifact storage as an add-on to the static product.

### How it differs from the default static mode

| Aspect | Static (default) | Self-hosted |
|--------|------------------|-------------|
| Storage | URL fragment only | SQLite + UUID |
| Server | None (static files) | Express server |
| Payload limits | 8,000 char fragment budget | 1 MB per artifact |
| Persistence | None (zero-retention) | 24h sliding TTL |
| Links | `host/#agent-render=v1...` | `host/{uuid}` |
| Dependencies | None beyond static hosting | Node.js, SQLite |

### Architecture

The self-hosted server:
- Builds the same Next.js static export and serves it from `out/`
- Adds Express API routes (`/api/artifacts`) for CRUD operations
- Handles `/{uuid}` routes by injecting the stored payload into the static HTML template via `window.__AGENT_RENDER_ENVELOPE__`
- The viewer shell checks for this injected global on mount and uses it instead of fragment decoding when present
- SQLite stores `id -> payload` mappings with timestamps and TTL tracking
- Expired artifacts are filtered at query time and can be cleaned up explicitly

### Viewer integration

The viewer shell (`src/components/viewer-shell.tsx`) checks for `window.__AGENT_RENDER_ENVELOPE__` on mount via the `resolveInjectedEnvelope()` helper in `src/lib/payload/injected.ts`. When present and valid, the injected envelope is used directly, bypassing fragment decoding. When absent, the standard fragment-based path runs as before.

This integration is minimal and non-breaking: the injected path only activates when the self-hosted server has set the global, which never happens in the static export.

### Key files

- `selfhosted/src/server.ts` - Express server with API routes and viewer route
- `selfhosted/src/db.ts` - SQLite database setup and queries
- `selfhosted/src/cleanup.ts` - Standalone expired artifact cleanup script
- `selfhosted/Dockerfile` - Multi-stage Docker build
- `selfhosted/docker-compose.yml` - Docker Compose configuration
- `src/lib/payload/injected.ts` - Injected envelope resolver for the viewer shell
- `skills/selfhosted-agent-render/SKILL.md` - Agent workflow skill
8 changes: 8 additions & 0 deletions docs/dependency-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
- `papaparse` plus `@tanstack/react-table` keeps CSV parsing and rendering readable without coupling to a heavyweight data-grid framework.
- `fflate` provides portable deflate/inflate support across iOS Safari and Android Chromium without relying on browser-specific compression streams.

## Self-hosted mode (optional, in selfhosted/)

- `express` - MIT — minimal HTTP server for the self-hosted variant
- `better-sqlite3` - MIT — synchronous SQLite bindings for Node.js, used for artifact storage
- `tsx` - MIT — TypeScript execution for Node.js, used as the dev/runtime runner

These dependencies are only required for the self-hosted server and live in `selfhosted/package.json`, separate from the main app's dependencies.

## Notable removals

- `rehype-highlight` was removed after review because markdown fences now reuse the CodeMirror viewer stack directly.
Expand Down
100 changes: 100 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,103 @@ Cloudflare Pages works well with the current project shape.
- Environment variable: set `NEXT_PUBLIC_BASE_PATH` only if you intentionally deploy under a subpath

If you deploy at the domain root on Cloudflare Pages, leave `NEXT_PUBLIC_BASE_PATH` unset.

---

## Self-hosted mode (optional)

The self-hosted variant in `selfhosted/` adds server-backed SQLite storage and UUID-based artifact links. This is a separate add-on deployment, not a replacement for static hosting.

### When to use it

- Payloads exceed the 8,000-character fragment budget
- Chat platforms mangle long or Unicode-heavy URLs
- You want short, persistent `/{uuid}` links
- Agents need a simple API to create and manage artifacts

### Quick start

```bash
# Build the static frontend first
npm ci && npm run build

# Set up the self-hosted server
cd selfhosted
npm install
cp .env.example .env
# Edit .env as needed (BASE_URL, PORT, etc.)
npm start
```

The server starts at `http://localhost:3001`.

### Docker Compose

```bash
cd selfhosted
docker compose up -d
```

Set `BASE_URL` and `PORT` via environment variables or edit `docker-compose.yml`.

### Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `3001` | Server port |
| `DB_PATH` | `./data/agent-render.db` | SQLite database file path |
| `STATIC_DIR` | `../out` | Path to the built static export |
| `BASE_URL` | `http://localhost:3001` | Base URL for artifact links in API responses |
| `TTL_HOURS` | `24` | Artifact expiry TTL in hours |

### Storage

Uses SQLite with a single `artifacts` table:

| Column | Type | Description |
|--------|------|-------------|
| `id` | TEXT (PK) | UUID v4 |
| `payload` | TEXT | Envelope JSON |
| `created_at` | TEXT | ISO datetime |
| `updated_at` | TEXT | ISO datetime |
| `last_viewed_at` | TEXT | ISO datetime (nullable) |
| `expires_at` | TEXT | ISO datetime |

An index on `expires_at` supports efficient TTL queries.

### TTL behavior

- 24-hour sliding TTL by default (configurable via `TTL_HOURS`)
- Each successful view extends expiry by the configured TTL
- Expired artifacts return 404
- Cleanup: `POST /api/cleanup` or `cd selfhosted && npm run cleanup`
- You can also ask your agent to clean up old DB records on a schedule

### API

- `POST /api/artifacts` — create an artifact (returns UUID and URL)
- `GET /api/artifacts/:id` — retrieve an artifact (refreshes TTL)
- `PUT /api/artifacts/:id` — update an artifact's payload
- `DELETE /api/artifacts/:id` — delete an artifact
- `POST /api/cleanup` — remove expired artifacts

### Daemon/service deployment

For persistent deployments, use pm2, systemd, or similar:

```bash
# pm2
cd selfhosted && pm2 start "node --import tsx src/server.ts" --name agent-render

# systemd: see skills/selfhosted-agent-render/SKILL.md for a unit file example
```

### Optional auth and perimeter protection

The server does not include built-in auth. For private deployments, consider:

- **Cloudflare Tunnel + Zero Trust**: install `cloudflared`, create a tunnel to your server, and configure Access policies. This is the recommended approach for exposing the server to the internet with identity-based access control.
- **Reverse proxy with auth**: nginx, Caddy, or Traefik with OAuth2 Proxy or basic auth in front of the server.
- **Localhost binding**: for same-machine deployments, the server listens on all interfaces by default. Use a reverse proxy or firewall to restrict access if needed.

The server can also be made fully public if desired.
13 changes: 13 additions & 0 deletions docs/payload-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,16 @@ Real diff artifacts can contain multiple `diff --git` sections inside one `patch
```

Malformed JSON should still use `kind: "json"`; the viewer will show the parse error and a raw fallback instead of crashing.

## Self-hosted payload storage

The optional self-hosted variant (`selfhosted/`) stores the same envelope JSON in SQLite under UUID v4 keys instead of encoding it into URL fragments.

When storing payloads for the self-hosted server:
- Use the standard envelope format documented above
- Set `codec` to `"plain"` (no fragment encoding is needed)
- The `POST /api/artifacts` endpoint accepts the envelope as a JSON object or string in the `payload` field
- Maximum payload size: 1 MB (vs. 8,000 characters for fragment-based links)
- The viewer renders the stored payload identically to a fragment-decoded payload

This mode is for use cases where fragment-length limits or URL mangling make fragment-based sharing impractical. See `docs/deployment.md` and `skills/selfhosted-agent-render/SKILL.md` for details.
14 changes: 14 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ The suite is intentionally split by responsibility:
- component tests protect selector/disclosure UI contracts
- unit tests protect transport codecs, envelope validation, diff parsing, and language inference

## Self-hosted server tests

Unit tests for the injected envelope resolver live alongside the existing test suite:

```bash
npm run test -- tests/injected.test.ts
```

The self-hosted server (`selfhosted/`) can be type-checked separately:

```bash
cd selfhosted && npm run typecheck
```

## CI

The repository includes `.github/workflows/test.yml`, which installs Playwright browsers and runs `npm run test:ci` on pushes, pull requests, and manual dispatch.
14 changes: 14 additions & 0 deletions selfhosted/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Port for the self-hosted server
PORT=3001

# Path to the SQLite database file
DB_PATH=./data/agent-render.db

# Path to the built static export from the main app (npm run build in repo root)
STATIC_DIR=../out

# Base URL for generating artifact links in API responses (no trailing slash)
BASE_URL=http://localhost:3001

# TTL in hours for artifact expiry (default: 24)
TTL_HOURS=24
4 changes: 4 additions & 0 deletions selfhosted/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
data/
dist/
*.db
39 changes: 39 additions & 0 deletions selfhosted/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
FROM node:20-slim AS builder

WORKDIR /app

# Build the static frontend
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build

# Set up the self-hosted server
FROM node:20-slim

WORKDIR /app/selfhosted

# Copy the built static output
COPY --from=builder /app/out /app/out

# Install server dependencies
COPY selfhosted/package.json selfhosted/package-lock.json* ./
RUN npm ci --omit=dev
Comment on lines +20 to +21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a selfhosted lockfile or stop using npm ci

This image copies only selfhosted/package.json and then runs npm ci --omit=dev, but rg --files selfhosted shows there is no selfhosted/package-lock.json in the repo. npm ci exits with EUSAGE without a lockfile, so the documented Dockerfile / Compose deployment path fails before the image is even built.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install the runtime loader used by npm start

CMD ["npm", "start"] resolves to node --import tsx src/server.ts in selfhosted/package.json, but this image installs with npm ci --omit=dev, which drops tsx because it is only listed under devDependencies. Even after fixing the lockfile problem, the container will still exit at startup because the TypeScript loader it relies on is missing from the runtime image.

Useful? React with 👍 / 👎.


# Copy server source
COPY selfhosted/src ./src
COPY selfhosted/tsconfig.json ./

# Create data directory for SQLite
RUN mkdir -p /app/selfhosted/data

ENV PORT=3001
ENV STATIC_DIR=/app/out
ENV DB_PATH=/app/selfhosted/data/agent-render.db
ENV BASE_URL=http://localhost:3001

EXPOSE 3001

VOLUME ["/app/selfhosted/data"]

CMD ["npm", "start"]
19 changes: 19 additions & 0 deletions selfhosted/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
agent-render:
build:
context: ..
dockerfile: selfhosted/Dockerfile
ports:
- "${PORT:-3001}:3001"
volumes:
- agent-render-data:/app/selfhosted/data
environment:
- PORT=3001
- STATIC_DIR=/app/out
- DB_PATH=/app/selfhosted/data/agent-render.db
- BASE_URL=${BASE_URL:-http://localhost:3001}
- TTL_HOURS=${TTL_HOURS:-24}
restart: unless-stopped

volumes:
agent-render-data:
Loading
Loading