Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ dist
playwright-report/
test-results/

# Self-hosted SQLite (local runs)
/data/

# yarn v2
.yarn/cache
.yarn/unplugged
Expand Down
31 changes: 28 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ Core product traits right now:
- self-hostable
- static-export friendly
- fragment-based transport so artifact contents stay out of the request URL and off the server request path
- optional **separate** self-hosted Node + SQLite server for UUID links (same viewer bundle with `NEXT_PUBLIC_SELFHOSTED_SERVER=1`; see `docs/deployment.md`)

## Product contract

Treat these as core constraints unless the owner explicitly changes the product direction.

### Default static export (primary product)

- The app is a single exported client-side shell, not a backend product.
- Artifact payloads live in the URL fragment, using `#agent-render=v1.<codec>.<payload>` for `plain|lz|deflate`, and `#agent-render=v1.arx.<dictVersion>.<payload>` for `arx`.
- The deployed host should not receive artifact contents as part of the initial page request.
Expand All @@ -30,13 +33,20 @@ Treat these as core constraints unless the owner explicitly changes the product
- The product is zero-retention by host design, not secret-safe in an absolute sense.
- Links may still leak through browser history, copied URLs, screenshots, and any future client-side analytics.

Do not casually introduce:
Do not casually introduce into the **default static path**:
- server persistence
- databases
- auth requirements for the core viewing path
- request-body upload flows for the main sharing workflow
- normal query-param transport for artifact contents

### Optional self-hosted UUID mode (explicit add-on)

- Ships as `selfhosted/server.mjs` + SQLite; stores the canonical `agent-render=v1...` string keyed by UUID v4; sliding 24h TTL on successful `GET /api/artifacts/:id`.
- Build the static bundle with `NEXT_PUBLIC_SELFHOSTED_SERVER=1` so `/{uuid}` routes fetch from the API; leave unset for normal static deployments.
- Same artifact kinds, codecs, decode, and renderers as the fragment product; no new envelope schema.
- Auth is perimeter-only; document Cloudflare Tunnel / Zero Trust as optional hardening, not requirements.

## Current shipped behavior

Describe and preserve what is already true in the repo today.
Expand Down Expand Up @@ -102,7 +112,7 @@ If you change the payload contract, update the code, docs, examples, and the Ope
## Key files

### App shell and UI
- `src/components/viewer-shell.tsx` - main shell, fragment-driven state, empty state, artifact-stage layout
- `src/components/viewer-shell.tsx` - main shell, fragment-driven state, optional UUID fetch path, empty state, artifact-stage layout
- `src/components/viewer/artifact-selector.tsx` - bundle artifact switching UI
- `src/components/viewer/fragment-details-disclosure.tsx` - fragment inspector/status disclosure
- `src/components/home/link-creator.tsx` - browser-side link creation UX
Expand All @@ -114,9 +124,15 @@ If you change the payload contract, update the code, docs, examples, and the Ope
- `src/components/renderers/csv-renderer.tsx`
- `src/components/renderers/json-renderer.tsx`

### Optional self-hosted server
- `selfhosted/server.mjs` - static file + API + `/{uuid}` shell routing
- `selfhosted/artifact-db.mjs` - SQLite schema, sliding TTL, CRUD
- `selfhosted/cleanup.mjs` - batch expiry purge
- `src/lib/selfhosted/artifact-path.ts` - UUID path parsing helper for the client

### Payload and protocol
- `src/lib/payload/schema.ts` - type surface, limits, fragment key, supported kinds/codecs
- `src/lib/payload/fragment.ts` - encode/decode logic and transport behavior
- `src/lib/payload/fragment.ts` - encode/decode logic and transport behavior (optional stored-mode decode skips fragment wire budget)
- `src/lib/payload/arx-codec.ts` - arx codec: domain dictionary + brotli + base76/base1k/baseBMP encoding
- `public/arx-dictionary.json` - shared substitution dictionary for the arx codec (served as a static endpoint)
- `public/arx-dictionary.json.br` - pre-compressed brotli variant of the dictionary
Expand All @@ -136,6 +152,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 All @@ -160,6 +177,13 @@ NEXT_PUBLIC_BASE_PATH=/agent-render npm run build
npm run preview
```

Optional self-hosted server (after `NEXT_PUBLIC_SELFHOSTED_SERVER=1 npm run build`):

```bash
npm run selfhosted:start
npm run selfhosted:cleanup
```

Validation:

```bash
Expand Down Expand Up @@ -217,6 +241,7 @@ At minimum, verify alignment across:
- `docs/dependency-notes.md`
- `docs/testing.md`
- `skills/agent-render-linking/SKILL.md`
- `skills/selfhosted-agent-render/SKILL.md`

## Default contributor stance

Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# agent-render

`agent-render` is a fully static, zero-retention artifact viewer for AI-generated outputs.
`agent-render` is a fully static, zero-retention artifact viewer for AI-generated outputs (with an **optional** self-hosted UUID mode for agents who need server-backed links).

Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based sharing for markdown, code, diffs, CSV, and JSON so the payload stays in the browser URL fragment instead of being sent to a server.
Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based sharing for markdown, code, diffs, CSV, and JSON so the payload stays in the browser URL fragment instead of being sent to a server on the default static path.

## OpenClaw

Expand Down Expand Up @@ -30,9 +30,10 @@ 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
- Fragment-based payloads (`#...`) so the server never receives artifact contents
- Fully static export with Next.js App Router for the **default** product
- No backend, no database, and no server-side persistence on the **static** path
- Fragment-based payloads (`#...`) so the static host never receives artifact contents during the page request
- Optional **self-hosted** UUID + SQLite mode for agents (separate Node server in `selfhosted/`; see `docs/deployment.md`)
- Public-safe naming and MIT-compatible dependencies

## Local Development
Expand All @@ -53,6 +54,10 @@ npm run preview

Set `NEXT_PUBLIC_BASE_PATH` before `npm run build` when you want to preview a subpath deployment locally.

## Optional self-hosted mode (UUID links)

Power users can run a small Node server that stores the same `agent-render=v1...` payload string in SQLite and opens it from `https://your-host/{uuid}/`. This **does not** replace the static fragment product: build with `NEXT_PUBLIC_SELFHOSTED_SERVER=1`, then `npm run selfhosted:start` after `npm run build`. Full notes live in `docs/deployment.md` and `skills/selfhosted-agent-render/SKILL.md`.

## Contributing

- Public exported functions/components in `src/lib/**` and `src/components/**` must have a preceding `/** ... */` JSDoc block.
Expand Down Expand Up @@ -85,9 +90,9 @@ The shell keeps first load lean and defers renderer-heavy code until needed. The

## Zero Retention

The project keeps artifact contents in the URL fragment so the static host does not receive the payload during the page request. This improves privacy for shared artifacts, but the link still lives in browser history, copied URLs, and any client-side telemetry you add later.
On the **default static deployment**, artifact contents stay in the URL fragment so the static host does not receive the payload during the page request. This improves privacy for shared artifacts, but the link still lives in browser history, copied URLs, and any client-side telemetry you add later.

`Zero Data Retention by design` means the deployed static host does not receive artifact contents as part of the request. It does not mean the data disappears from places like browser history, copied links, screenshots, or any client-side analytics you may add later.
`Zero Data Retention by design` in the static UI refers to that static-host boundary. The **optional self-hosted** server intentionally stores payloads in SQLite with a sliding TTL; treat that as a different deployment contract (see `docs/deployment.md`).

## License

Expand Down
7 changes: 4 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

`agent-render` is a single exported client-side shell built with Next.js 15, React 19, and Tailwind CSS 4.

- The application ships as static files only.
- All artifact data lives in the URL fragment.
- The default product ships as static files only.
- In the default product, artifact data lives in the URL fragment.
- An **optional** self-hosted Node server (see `docs/deployment.md`) can store the same canonical payload string in SQLite and serve `GET /{uuid}` with the **same** viewer bundle; this is off by default and requires `NEXT_PUBLIC_SELFHOSTED_SERVER=1` at build time.
- The app renders one viewer shell and selects a renderer based on the artifact kind.
- Renderers stay modular so they can evolve independently without coupling to Next.js routing.

Expand Down Expand Up @@ -92,7 +93,7 @@ The fragment protocol keeps the JSON envelope stable and treats compression stri
- packed wire mode (`p: 1`) shortens transport keys before compression, then unpacks back to the standard envelope during decode
- automatic async codec selection tries `arx -> deflate -> lz -> plain` and compares packed + non-packed candidates
- sync codec selection (used by examples and legacy paths) tries `deflate -> lz -> plain`
- decode enforces both fragment length and decoded payload size ceilings before UI rendering
- decode enforces the fragment wire budget and decoded payload size ceilings before UI rendering on the static path; optional server-stored payloads may skip the wire budget while keeping the decoded ceiling
- invalid bundle state is normalized or rejected before renderers mount

## Zero-retention boundaries
Expand Down
2 changes: 2 additions & 0 deletions docs/dependency-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- `@tanstack/react-table` - MIT
- `lz-string` - MIT
- `fflate` - MIT
- `better-sqlite3` - MIT (optional; used only by the self-hosted Node server under `selfhosted/`)

## Notes

Expand All @@ -30,6 +31,7 @@
- `@git-diff-view/*` fits review-style diffs better than a generic merge editor for the current viewer.
- `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.
- `better-sqlite3` keeps the optional self-hosted server simple with a local SQLite file and synchronous reads for the CRUD API.

## Notable removals

Expand Down
71 changes: 71 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,74 @@ 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.

## Optional self-hosted mode (UUID links + SQLite)

This is a **separate deployment shape** from static fragment hosting. It targets agents and operators who accept a Node runtime, a SQLite file on disk, and **server-side retention** with a **24-hour sliding TTL** (each successful `GET /api/artifacts/:id` extends expiry by another 24h). It is **not** the default product and does not replace Cloudflare Pages–style static hosting.

### How it differs from the static product

| | Static (default) | Self-hosted (optional) |
| --- | --- | --- |
| Runtime | Static files only | Node.js HTTP server |
| Payload location | URL fragment | SQLite keyed by UUID v4 |
| Host sees payload | No (fragment not sent) | Yes (stored server-side) |
| Viewer UI | This repo’s shell | Same shell + build flag |
| Auth | None required | None built-in; use perimeter controls |

### Build the client bundle for UUID routes

The static export must include the self-hosted client switch:

```bash
NEXT_PUBLIC_SELFHOSTED_SERVER=1 npm run build
```

Omit this variable for normal static deployments (including `agent-render.com`). When the flag is absent, `/{uuid}` paths are not treated as artifact routes in the client.

### Run the server

From the repository root (after `npm run build`):

```bash
npm run selfhosted:start
```

Environment variables:

- `PORT` — listen port (default `3000`)
- `DATABASE_PATH` — SQLite file path (default `./data/artifacts.sqlite`)
- `STATIC_ROOT` — directory containing the static export (default `./out`)
- `NEXT_PUBLIC_BASE_PATH` — must match how the static assets were built (same as static deployment)

### HTTP API

All routes respect `NEXT_PUBLIC_BASE_PATH` when set.

- `POST /api/artifacts` — body `{ "payload": "agent-render=v1...." }` → `201` with `id`, `createdAt`, `expiresAt`
- `GET /api/artifacts/:id` — JSON metadata plus `payload`; refreshes sliding TTL
- `PUT /api/artifacts/:id` — replace `payload`, refresh TTL
- `DELETE /api/artifacts/:id` — remove row
- `GET /{uuid}` — serves `index.html` so the client can fetch the artifact API

### TTL and cleanup

- Expired artifacts return `404` with `{ "error": "expired" }` and are removed on read.
- Run `npm run selfhosted:cleanup` on a schedule to delete expired rows in batch.
- Operators can ask an agent to delete specific IDs or vacuum old data.

### Docker Compose

```bash
docker compose -f selfhosted/docker-compose.yml up --build
```

The image runs `npm run build` with `NEXT_PUBLIC_SELFHOSTED_SERVER=1` unless you override build args.

### Perimeter protection (practical, neutral)

Bind to loopback, use a private network, terminate TLS and auth at a reverse proxy, or place the service behind **Cloudflare Tunnel** with optional **Zero Trust** access policies. Public exposure is possible if you deliberately choose it; document retention and TTL for your users.

### Same-machine / agent co-location

A common pattern is to run the server on `127.0.0.1` next to the agent process so share URLs stay on localhost without exposing the SQLite file to the internet.
6 changes: 5 additions & 1 deletion docs/payload-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

## Goals

The project uses a fragment-based payload so the raw artifact content stays in the browser and is not sent to the server during the request.
The default product uses a fragment-based payload so the raw artifact content stays in the browser and is not sent to the server during the request.

## Optional self-hosted UUID transport

An **optional** deployment (see `docs/deployment.md`) can store the exact same `agent-render=v1...` wire string in SQLite and load it by UUID. That path bypasses the **fragment wire size budget** while decoding, but the **decoded JSON budget** (`MAX_DECODED_PAYLOAD_LENGTH`, currently `200000` characters) still applies in the viewer. This is intended for power users and agents who accept server retention with a sliding TTL.

## Fragment shape

Expand Down
3 changes: 3 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ npm run test:e2e
npm run test:e2e:update
npm run test:browsers
npm run test:ci
npm run selfhosted:cleanup
```

Self-hosted integration is covered by `tests/selfhosted-artifact-db.test.ts` (SQLite store) and fragment decode tests for stored-mode wire lengths. End-to-end Playwright suites target the **static** export only (no `NEXT_PUBLIC_SELFHOSTED_SERVER` flag).

## Browser install

Before running Playwright locally for the first time:
Expand Down
Loading
Loading