Skip to content

Add optional self-hosted UUID mode (SQLite + server) reusing the viewer#17

Open
baanish wants to merge 1 commit intomainfrom
codex/add-self-hosted-app-with-uuid-links
Open

Add optional self-hosted UUID mode (SQLite + server) reusing the viewer#17
baanish wants to merge 1 commit intomainfrom
codex/add-self-hosted-app-with-uuid-links

Conversation

@baanish
Copy link
Owner

@baanish baanish commented Mar 20, 2026

Motivation

  • Provide an optional server-backed deployment that stores existing agent-render payload strings under UUID v4 ids so operators/agents can share short https://host/{uuid} links when fragments are impractical.
  • Keep the shipped static fragment viewer as the main/default product and reuse its decoding/rendering logic for the self-hosted path to preserve feature parity.

Description

  • Add a minimal Node HTTP server with CRUD API and UUID route handling in server/selfhosted-app.ts and server/selfhosted.ts, plus a manual cleanup command server/cleanup-selfhosted.ts, exposing POST /api/artifacts, GET/PUT/DELETE /api/artifacts/:id and GET /:id which injects the stored payload into the exported viewer HTML. Sliding 24-hour TTL and lazy/manual cleanup are implemented.
  • Implement a SQLite-backed store under src/lib/selfhosted/ (store.ts, constants.ts, stored-payload.ts) which persists the canonical payload string, enforces UUID v4 ids, and performs TTL refresh on read.
  • Reuse the existing viewer by adding a bootstrap reader and integration: src/lib/selfhosted/bootstrap.ts and updates to src/components/viewer-shell.tsx so the viewer accepts an injected stored payload, preserves renderer behavior (copy/download/print, diff modes, artifact switching) and keeps artifact switching local in UUID mode.
  • Add tests covering store and server behaviors and the viewer bootstrap path under tests/selfhosted/* and tests/components/viewer-shell-selfhosted.test.tsx, update test/setup/config (tests/setup.tsx, vitest.config.ts, tsconfig.json) and add a new skill and docs (skills/selfhosted-agent-render/SKILL.md, updates to README.md, docs/*, and skills/agent-render-linking/SKILL.md) explaining differences between static fragment mode and the optional self-hosted mode.

Testing

  • npm run typecheck — ✅ passed.
  • npm run lint — ✅ passed (includes public-export docs check).
  • npm test — ✅ passed (Vitest suite including new self-hosted store/server/viewer tests ran; all tests passed).
  • Started the self-hosted server locally to validate runtime behavior with PORT=4010 AGENT_RENDER_DB_PATH=.data/test-selfhosted.sqlite node --experimental-strip-types server/selfhosted.ts — ✅ server started and accepted test flows.
  • npm run build⚠️ build failed in this environment due to Next.js font downloads failing (network/font fetch limitation); this is an environment limitation and not specific to the new self-hosted code.

Automated test coverage added/verified: CRUD flows, UUID lookup, expired-item behavior, TTL refresh-on-read, HTML bootstrap injection for /{uuid}, and rendering a stored payload through the shared viewer path.


Codex Task

Summary by CodeRabbit

  • New Features

    • Introduced optional self-hosted artifact storage mode with UUID-based short links as an alternative to the default static fragment-based experience.
    • Added API endpoints for artifact management (create, retrieve, update, delete).
    • Artifacts in self-hosted mode expire after 24 hours with automatic refresh on access.
  • Documentation

    • Updated guides to reflect two deployment modes: static fragment-first (zero-retention) and optional self-hosted (time-limited storage).
    • Added architectural documentation, deployment instructions, and operational guidance for self-hosted deployments.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 20, 2026

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: d4ccaf7
Status: ✅  Deploy successful!
Preview URL: https://1577d374.agent-render.pages.dev
Branch Preview URL: https://codex-add-self-hosted-app-wi.agent-render.pages.dev

View logs

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

The PR introduces a self-hosted UUID mode for agent-render, extending the platform from a static fragment-only artifact viewer to support optional server-backed persistence. The deployment now offers two modes: static fragment mode (default, zero-retention by design) and self-hosted UUID mode (Node.js + SQLite with 24-hour sliding TTL), reusing the same viewer shell for both.

Changes

Cohort / File(s) Summary
Documentation & specification
AGENTS.md, README.md, docs/architecture.md, docs/dependency-notes.md, docs/deployment.md, docs/payload-format.md, docs/testing.md
Extensive documentation updates clarifying the shift from "fully static" to "fragment-first" with optional self-hosted mode, including deployment guidance, TTL behavior (24-hour sliding), security posture distinctions between static/self-hosted, and test coverage expectations for the new storage layer.
Skills guides
skills/agent-render-linking/SKILL.md, skills/selfhosted-agent-render/SKILL.md
Updated fragment-linking skill to defer users to self-hosted mode when appropriate; added comprehensive self-hosted skill covering API workflow, TTL semantics, CRUD operations, and operational deployment patterns (Docker Compose, daemon managers, Cloudflare Tunnel).
Server entrypoints & lifecycle
server/selfhosted.ts, server/selfhosted-app.ts, server/cleanup-selfhosted.ts
New Node.js HTTP server implementation with environment-based configuration, SQLite database initialization, RESTful artifact API (POST/GET/PUT/DELETE /api/artifacts/:id), UUID fragment route handling, HTML bootstrap injection, and cleanup utility for expired records.
Self-hosted storage & validation
src/lib/selfhosted/store.ts, src/lib/selfhosted/stored-payload.ts, src/lib/selfhosted/bootstrap.ts, src/lib/selfhosted/constants.ts
SQLite-backed SelfHostedArtifactStore class with CRUD and TTL refresh semantics; payload validation and normalization utilities; bootstrap injection type definitions and window interface augmentation; TTL and UUID constants.
Viewer shell bootstrap support
src/components/viewer-shell.tsx
Modified viewer-shell to support bootstrap mode: hash-change synchronization gated by stored bootstrap presence, payload decoding switched to stored payload when bootstrap exists, artifact selection updates local state (no URL encoding) in bootstrap mode, and conditional navigation behavior (/ vs hash clear).
Configuration & build
package.json, tsconfig.json, vitest.config.ts
Added npm scripts for self-hosted server (start:selfhosted, cleanup:selfhosted, build:selfhosted); enabled TypeScript import of .ts extensions; added JSX automatic transformation to Vitest esbuild config.
Tests
tests/selfhosted/store.test.ts, tests/selfhosted/server.test.ts, tests/components/viewer-shell-selfhosted.test.tsx, tests/setup.tsx
New test suites validating SQLite store lifecycle (create/read/update/delete/expiration), full server HTTP endpoints with TTL refresh, bootstrap HTML injection, and viewer-shell rendering in UUID mode; updated image mock to handle priority and unoptimized props.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser Client
    participant Server as Node.js Server
    participant SQLite as SQLite DB
    participant Static as Static Assets

    Client->>Server: POST /api/artifacts<br/>{payload: "..."}
    Server->>SQLite: INSERT artifact<br/>(id, payload, expires_at)
    SQLite-->>Server: Stored artifact record
    Server-->>Client: 201 {id, url, expiresAt}

    Client->>Server: GET /:uuid
    Server->>SQLite: SELECT * WHERE id=:uuid
    SQLite-->>Server: Artifact record (if not expired)
    Server->>Static: Read index.html
    Static-->>Server: HTML content
    Server->>Server: Inject <script><br/>window.__AGENT_RENDER_STORED_PAYLOAD__
    Server-->>Client: HTML with embedded payload

    Client->>Server: GET /api/artifacts/:uuid
    Server->>SQLite: UPDATE last_viewed_at,<br/>expires_at (TTL refresh)
    SQLite-->>Server: Updated record
    Server-->>Client: 200 {id, payload, expiresAt}
Loading
sequenceDiagram
    participant User as User/Agent
    participant Client as Browser Client
    participant Store as SelfHostedArtifactStore
    participant SQLite as SQLite DB

    User->>Client: Create artifact payload
    Client->>Store: create(payload: string)
    Store->>SQLite: INSERT INTO artifacts<br/>(id, payload, created_at,<br/>expires_at = now + 24h)
    SQLite-->>Store: Artifact ID (UUID v4)
    Store-->>Client: StoredArtifactRecord

    Note over Client,Store: Time passes (< 24h)
    Client->>Store: getById(id,<br/>{refreshTtl: true})
    Store->>SQLite: UPDATE expires_at,<br/>last_viewed_at
    SQLite-->>Store: Refreshed record
    Store-->>Client: StoredArtifactRecord

    Note over Client,Store: Time passes (> 24h from last view)
    Client->>Store: getById(id)
    Store->>SQLite: SELECT * WHERE id=id
    SQLite-->>Store: Expired record (expires_at < now)
    Store->>SQLite: DELETE WHERE id=id
    Store-->>Client: null (artifact unavailable)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A fragment-first viewer hops through the code,
Now carrying payloads on two different roads:
Static or stored in a SQLite den,
Twenty-four hours to view them again!
Bootstrap injection, UUID links bright—
The artifact rabbit runs left and right. 🎯

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature addition: an optional self-hosted UUID mode using SQLite and a server, with emphasis on reusing the viewer shell.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/add-self-hosted-app-with-uuid-links

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4ccaf75c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +285 to +286
const payloadString = storedBootstrap ? getDisplayPayloadString(storedBootstrap.payload) : hash;
const [parsed, setParsed] = useState<ReturnType<typeof decodeFragment>>(() => decodeFragment(payloadString));

Choose a reason for hiding this comment

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

P1 Badge Skip fragment-size checks for bootstrapped UUID payloads

This now feeds storedBootstrap.payload back through decodeFragment()/decodeFragmentAsync(), and those helpers still reject anything over MAX_FRAGMENT_LENGTH in src/lib/payload/fragment.ts:266-268. The result is that the new UUID mode cannot render the very payloads it is advertised for: a stored payload longer than 8,000 characters can be created successfully, but opening /{uuid} will only land in the too-large error state because it is treated like a URL fragment again instead of a server-delivered payload.

Useful? React with 👍 / 👎.

Comment on lines +64 to +67
function injectStoredPayload(indexHtml: string, artifact: { id: string; payload: string; expiresAt: string }): string {
const bootstrap = JSON.stringify({ id: artifact.id, payload: artifact.payload, expiresAt: artifact.expiresAt });
const script = `<script>window.__AGENT_RENDER_STORED_PAYLOAD__=${bootstrap};</script>`;
return indexHtml.replace("</head>", `${script}</head>`);

Choose a reason for hiding this comment

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

P1 Badge Escape stored payloads before injecting the bootstrap script

injectStoredPayload() writes artifact.payload directly into an inline <script>, but validateStoredPayloadString() only checks the outer prefix in src/lib/selfhosted/stored-payload.ts:40-41. A request body such as agent-render=v1.arx.0.</script><script>... is therefore accepted and then breaks out of this script block when someone opens the UUID link, executing attacker-controlled JavaScript. This matters anywhere the self-hosted create/update API is reachable by untrusted clients or shared automation.

Useful? React with 👍 / 👎.

@kilo-code-bot
Copy link

kilo-code-bot bot commented Mar 20, 2026

Code Review Summary

Status: 1 Suggestion Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
src/components/viewer-shell.tsx 488 The "Zero Data Retention by design" pill is shown unconditionally, which is misleading in self-hosted UUID mode since the payload IS stored in SQLite. Consider hiding or modifying this pill when storedBootstrap is present, similar to how the "UUID mode" badge is conditionally shown on line 502.
Other Observations (not in diff)

The implementation is solid overall. The self-hosted mode correctly:

  • Uses UUID v4 validation
  • Has path traversal protection in static file serving
  • Validates payload format before storage
  • Implements 24-hour sliding TTL
  • Properly injects bootstrap payload for viewer rendering
  • Has comprehensive test coverage for CRUD, TTL refresh, and expiry handling

The documentation is thorough and correctly explains the distinction between static fragment mode (zero-retention) and self-hosted mode (SQLite-backed).

Files Reviewed (24 files)
  • AGENTS.md - Documentation updates
  • README.md - Documentation updates
  • docs/architecture.md - Documentation updates
  • docs/dependency-notes.md - Documentation updates
  • docs/deployment.md - Documentation updates
  • docs/payload-format.md - Documentation updates
  • docs/testing.md - Documentation updates
  • package.json - npm scripts
  • server/cleanup-selfhosted.ts - Cleanup script
  • server/selfhosted-app.ts - HTTP server
  • server/selfhosted.ts - Server entrypoint
  • skills/agent-render-linking/SKILL.md - Skill updates
  • skills/selfhosted-agent-render/SKILL.md - New skill
  • src/components/viewer-shell.tsx - Viewer shell with self-hosted support
  • src/lib/selfhosted/bootstrap.ts - Bootstrap reader
  • src/lib/selfhosted/constants.ts - TTL and UUID constants
  • src/lib/selfhosted/store.ts - SQLite store
  • src/lib/selfhosted/stored-payload.ts - Payload validation
  • tests/components/viewer-shell-selfhosted.test.tsx - Viewer tests
  • tests/selfhosted/server.test.ts - Server tests
  • tests/selfhosted/store.test.ts - Store tests
  • tests/setup.tsx - Test setup
  • tsconfig.json - TypeScript config
  • vitest.config.ts - Vitest config

Reviewed by minimax-m2.5-20260211 · 1,671,235 tokens

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package.json (1)

75-77: ⚠️ Potential issue | 🟠 Major

Node engine constraint doesn't cover self-hosted mode requirements.

The engines.node field declares >=20.10.0, but the self-hosted mode's dependency on node:sqlite (imported in src/lib/selfhosted/store.ts) requires Node 22.5+ with --experimental-sqlite flag, or Node 23+ where it's available without flags.

This mismatch could lead to confusing runtime errors for operators who attempt self-hosted deployment on Node 20 or 21.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 75 - 77, The engines.node constraint in
package.json must be raised to match the requirements of the self-hosted SQLite
import (node:sqlite used in src/lib/selfhosted/store.ts); update the
"engines.node" field from ">=20.10.0" to a minimum that guarantees native sqlite
support (e.g., ">=23.0.0") or to ">=22.5.0" if you prefer to require the
experimental flag, and also update any deployment/docs referencing Node
requirements to reflect this change so operators won't run unsupported Node
versions.
🧹 Nitpick comments (7)
server/selfhosted.ts (1)

5-5: Consider validating PORT for non-numeric values.

If PORT is set to an invalid value (e.g., "abc"), Number() returns NaN, which will cause server.listen to fail with a potentially unclear error. A simple guard could improve operator experience:

🛡️ Suggested validation
 const port = Number(process.env.PORT ?? 3000);
+if (Number.isNaN(port) || port < 1 || port > 65535) {
+  console.error(`Invalid PORT: ${process.env.PORT}. Must be a number between 1 and 65535.`);
+  process.exit(1);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/selfhosted.ts` at line 5, The PORT environment value is not validated:
the const port = Number(process.env.PORT ?? 3000) can become NaN for non-numeric
strings and break server.listen; change the logic around the port variable to
parse and validate (e.g., parseInt or Number), check Number.isInteger and
!Number.isNaN and that the port is within valid range (1–65535), and if invalid
either fall back to the default (3000) or log a clear error and exit; update
references to the port variable (used in server.listen) so they use the
validated numeric value.
docs/dependency-notes.md (1)

37-37: Consider specifying the exact Node version requirement.

The note mentions node:sqlite is "experimental in current Node 22 runtimes" but doesn't clarify the minimum version (22.5+) or that Node 23+ includes it as stable. Operators may not know which "supported Node release" to use.

Suggested clarification:

-- `node:sqlite` is an experimental Node built-in as of current Node 22 runtimes; self-hosted operators should use a supported Node release that includes it.
+- `node:sqlite` requires Node 22.5+ (with `--experimental-sqlite` flag) or Node 23+; self-hosted operators should ensure their runtime meets this requirement.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/dependency-notes.md` at line 37, Update the note about node:sqlite to
state the exact version requirements: indicate that node:sqlite is available
experimentally starting in Node 22.5 and becomes stable in Node 23+, and
recommend using at least Node 22.5 (or Node 23+) for self-hosted operators; edit
the sentence that currently reads about "current Node 22 runtimes" to explicitly
mention "Node 22.5+ (experimental) or Node 23+ (stable)" and add a brief
recommendation to prefer a supported Node release meeting those minimums.
tests/selfhosted/store.test.ts (1)

52-52: Strengthen the UUID assertion to validate UUID v4 shape.

Line 52 currently accepts any 36-char hex/dash sequence. Tightening this assertion better protects the UUID v4 contract.

Proposed assertion update
-    expect(created.id).toMatch(/[0-9a-f-]{36}/i);
+    expect(created.id).toMatch(
+      /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/selfhosted/store.test.ts` at line 52, The test currently validates
created.id with a loose regex; tighten it to assert a UUID v4 shape by replacing
the current expect(created.id).toMatch(/[0-9a-f-]{36}/i) check with a stricter
UUIDv4 pattern (matching the 8-4-4-4-12 hex groups and the version/variant bits)
so the test only accepts valid UUID v4 strings for created.id.
src/lib/selfhosted/bootstrap.ts (1)

21-21: Add a runtime shape guard before returning the injected bootstrap object.

Line 21 returns whatever is on window.__AGENT_RENDER_STORED_PAYLOAD__; a malformed global could break downstream reads. Consider validating { id, payload, expiresAt } are strings before returning.

Proposed defensive guard
+function isStoredPayloadBootstrap(value: unknown): value is StoredPayloadBootstrap {
+  if (!value || typeof value !== "object") return false;
+  const candidate = value as Record<string, unknown>;
+  return (
+    typeof candidate.id === "string" &&
+    typeof candidate.payload === "string" &&
+    typeof candidate.expiresAt === "string"
+  );
+}
+
 /**
  * Reads the self-hosted bootstrap payload injected into the exported viewer page.
  */
 export function getStoredPayloadBootstrap(): StoredPayloadBootstrap | null {
   if (typeof window === "undefined") {
     return null;
   }
 
-  return window.__AGENT_RENDER_STORED_PAYLOAD__ ?? null;
+  const value = window.__AGENT_RENDER_STORED_PAYLOAD__;
+  return isStoredPayloadBootstrap(value) ? value : null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/selfhosted/bootstrap.ts` at line 21, The code currently returns
window.__AGENT_RENDER_STORED_PAYLOAD__ directly which can be malformed; add a
runtime shape guard in the exporter (the code that reads
window.__AGENT_RENDER_STORED_PAYLOAD__) to verify the value is a non-null object
with keys id, payload, expiresAt and that each is a string (or the expected
primitive type) before returning it, otherwise return null; keep the check
defensive (typeof checks and an object-null guard) and avoid throwing so
downstream code still receives null for invalid shapes.
tests/components/viewer-shell-selfhosted.test.tsx (1)

42-42: Make the explanatory-copy assertion less brittle.

Line 42 matches a full sentence exactly, so minor copy edits will fail behavior-valid tests. Prefer a shorter regex/key phrase assertion.

Example adjustment
-    expect(screen.getByText("Selecting an artifact updates the active viewer state locally while keeping the stored payload and UUID route unchanged.")).toBeInTheDocument();
+    expect(
+      screen.getByText(/active viewer state locally/i),
+    ).toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/components/viewer-shell-selfhosted.test.tsx` at line 42, The test's
assertion using screen.getByText with the full sentence is brittle; replace the
exact string match with a more resilient partial or regex match (e.g., use
screen.getByText(/Selecting an artifact/i) or another concise key phrase) so the
test still verifies the explanatory copy without depending on the entire
sentence; update the assertion around getByText("Selecting an artifact updates
the active viewer state locally while keeping the stored payload and UUID route
unchanged.") accordingly in tests/components/viewer-shell-selfhosted.test.tsx.
skills/selfhosted-agent-render/SKILL.md (1)

81-81: Use one route placeholder style consistently (:id vs {uuid}).

The doc mixes :id and {uuid} for the same endpoint family. Standardizing one style improves copy/paste clarity.

Example normalization
-DELETE /api/artifacts/{uuid}
+DELETE /api/artifacts/:id

Also applies to: 124-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/selfhosted-agent-render/SKILL.md` at line 81, The API docs in SKILL.md
use two different path placeholder styles (`:id` and `{uuid}`) for the same
endpoints (e.g., `DELETE /api/artifacts/:id` and occurrences at lines 124-125);
pick a single placeholder convention and apply it consistently across the file
(replace all `:id` with `{uuid}` or vice versa), updating every endpoint string
and any related examples or references (search for `:id`, `{uuid}`, and similar
placeholders) so all routes use the chosen style uniformly.
src/components/viewer-shell.tsx (1)

683-683: Minor formatting inconsistency.

The indentation on this line appears misaligned compared to surrounding code. This doesn't affect functionality but may cause linter warnings.

🔧 Suggested fix
-                      {sampleCards.map((sample) => {
+                  {sampleCards.map((sample) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/viewer-shell.tsx` at line 683, The line containing the JSX
iterator "{sampleCards.map((sample) => {" in the ViewerShell component is
mis-indented; adjust its indentation to match surrounding JSX blocks (align it
with sibling JSX elements inside the same parent) so the code formatting is
consistent with the rest of src/components/viewer-shell.tsx; locate the usage of
sampleCards.map in the render/return of the ViewerShell (or the component
function where sampleCards is mapped) and fix the whitespace/tabs to match the
surrounding lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 22-24: Update the Node engine constraint in package.json or add a
runtime guard in server/selfhosted.ts: either change the package.json
"engines.node" value to ">=22.5.0" so installs require a Node version that
supports node:sqlite, or add a startup check in server/selfhosted.ts (before
SelfHostedArtifactStore or any import/use of node:sqlite) that inspects
process.version and throws/logs a clear error instructing users to run Node
>=22.5.0; reference package.json::engines.node and server/selfhosted.ts /
SelfHostedArtifactStore / node:sqlite when making the change.

In `@server/selfhosted-app.ts`:
- Around line 108-133: The renderMissingArtifactPage function currently
interpolates the reason string directly into HTML, risking XSS if caller input
becomes untrusted; update renderMissingArtifactPage to HTML-escape the reason
before insertion (e.g., replace &, <, >, ", ' and / with safe entities) and use
that escaped value in the template; ensure the escaping utility is a small
helper (escapeHtml or similar) referenced and used inside
renderMissingArtifactPage so all callers (current and future) are protected.

In `@tests/selfhosted/server.test.ts`:
- Around line 1-9: The CI fails because src/lib/selfhosted/store.ts imports
DatabaseSync from the built-in node:sqlite (requires Node 22.5+); update the
implementation to use a Node 20-compatible SQLite library instead (or make the
import conditional). Replace the direct import of DatabaseSync from
"node:sqlite" in SelfHostedArtifactStore with a wrapper that uses better-sqlite3
(or sqlite3) — e.g., swap the import for the chosen package, adapt any
DatabaseSync-typed usages in the SelfHostedArtifactStore class and related
functions to the API of the new library (connection/open,
prepare/run/get/all/close), and ensure tests and exports that reference
createSelfHostedServer continue to work with the new store implementation;
alternatively, update CI to Node 22.5+ if you prefer to keep node:sqlite.

In `@tests/selfhosted/store.test.ts`:
- Around line 1-9: Update the Node.js requirement to meet node:sqlite's minimum
(v22.5.0): change the package.json engines.node entry (the "engines.node" field)
to require >=22.5.0 (or a recommended LTS range like ">=22.x" or ">=24.x"), and
update the CI configuration that sets the runner version (the workflow
"node-version" key in your GitHub Actions workflow) from 20 to a supported
version (22 or 24) so tests run with Node 22.5.0+.

---

Outside diff comments:
In `@package.json`:
- Around line 75-77: The engines.node constraint in package.json must be raised
to match the requirements of the self-hosted SQLite import (node:sqlite used in
src/lib/selfhosted/store.ts); update the "engines.node" field from ">=20.10.0"
to a minimum that guarantees native sqlite support (e.g., ">=23.0.0") or to
">=22.5.0" if you prefer to require the experimental flag, and also update any
deployment/docs referencing Node requirements to reflect this change so
operators won't run unsupported Node versions.

---

Nitpick comments:
In `@docs/dependency-notes.md`:
- Line 37: Update the note about node:sqlite to state the exact version
requirements: indicate that node:sqlite is available experimentally starting in
Node 22.5 and becomes stable in Node 23+, and recommend using at least Node 22.5
(or Node 23+) for self-hosted operators; edit the sentence that currently reads
about "current Node 22 runtimes" to explicitly mention "Node 22.5+
(experimental) or Node 23+ (stable)" and add a brief recommendation to prefer a
supported Node release meeting those minimums.

In `@server/selfhosted.ts`:
- Line 5: The PORT environment value is not validated: the const port =
Number(process.env.PORT ?? 3000) can become NaN for non-numeric strings and
break server.listen; change the logic around the port variable to parse and
validate (e.g., parseInt or Number), check Number.isInteger and !Number.isNaN
and that the port is within valid range (1–65535), and if invalid either fall
back to the default (3000) or log a clear error and exit; update references to
the port variable (used in server.listen) so they use the validated numeric
value.

In `@skills/selfhosted-agent-render/SKILL.md`:
- Line 81: The API docs in SKILL.md use two different path placeholder styles
(`:id` and `{uuid}`) for the same endpoints (e.g., `DELETE /api/artifacts/:id`
and occurrences at lines 124-125); pick a single placeholder convention and
apply it consistently across the file (replace all `:id` with `{uuid}` or vice
versa), updating every endpoint string and any related examples or references
(search for `:id`, `{uuid}`, and similar placeholders) so all routes use the
chosen style uniformly.

In `@src/components/viewer-shell.tsx`:
- Line 683: The line containing the JSX iterator "{sampleCards.map((sample) =>
{" in the ViewerShell component is mis-indented; adjust its indentation to match
surrounding JSX blocks (align it with sibling JSX elements inside the same
parent) so the code formatting is consistent with the rest of
src/components/viewer-shell.tsx; locate the usage of sampleCards.map in the
render/return of the ViewerShell (or the component function where sampleCards is
mapped) and fix the whitespace/tabs to match the surrounding lines.

In `@src/lib/selfhosted/bootstrap.ts`:
- Line 21: The code currently returns window.__AGENT_RENDER_STORED_PAYLOAD__
directly which can be malformed; add a runtime shape guard in the exporter (the
code that reads window.__AGENT_RENDER_STORED_PAYLOAD__) to verify the value is a
non-null object with keys id, payload, expiresAt and that each is a string (or
the expected primitive type) before returning it, otherwise return null; keep
the check defensive (typeof checks and an object-null guard) and avoid throwing
so downstream code still receives null for invalid shapes.

In `@tests/components/viewer-shell-selfhosted.test.tsx`:
- Line 42: The test's assertion using screen.getByText with the full sentence is
brittle; replace the exact string match with a more resilient partial or regex
match (e.g., use screen.getByText(/Selecting an artifact/i) or another concise
key phrase) so the test still verifies the explanatory copy without depending on
the entire sentence; update the assertion around getByText("Selecting an
artifact updates the active viewer state locally while keeping the stored
payload and UUID route unchanged.") accordingly in
tests/components/viewer-shell-selfhosted.test.tsx.

In `@tests/selfhosted/store.test.ts`:
- Line 52: The test currently validates created.id with a loose regex; tighten
it to assert a UUID v4 shape by replacing the current
expect(created.id).toMatch(/[0-9a-f-]{36}/i) check with a stricter UUIDv4
pattern (matching the 8-4-4-4-12 hex groups and the version/variant bits) so the
test only accepts valid UUID v4 strings for created.id.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e58bea96-3ac5-4db3-bd7c-94e3b405f4a8

📥 Commits

Reviewing files that changed from the base of the PR and between 3d5ed45 and d4ccaf7.

📒 Files selected for processing (24)
  • AGENTS.md
  • README.md
  • docs/architecture.md
  • docs/dependency-notes.md
  • docs/deployment.md
  • docs/payload-format.md
  • docs/testing.md
  • package.json
  • server/cleanup-selfhosted.ts
  • server/selfhosted-app.ts
  • server/selfhosted.ts
  • skills/agent-render-linking/SKILL.md
  • skills/selfhosted-agent-render/SKILL.md
  • src/components/viewer-shell.tsx
  • src/lib/selfhosted/bootstrap.ts
  • src/lib/selfhosted/constants.ts
  • src/lib/selfhosted/store.ts
  • src/lib/selfhosted/stored-payload.ts
  • tests/components/viewer-shell-selfhosted.test.tsx
  • tests/selfhosted/server.test.ts
  • tests/selfhosted/store.test.ts
  • tests/setup.tsx
  • tsconfig.json
  • vitest.config.ts

Comment on lines +22 to +24
"start:selfhosted": "node --experimental-strip-types server/selfhosted.ts",
"cleanup:selfhosted": "node --experimental-strip-types server/cleanup-selfhosted.ts",
"build:selfhosted": "npm run build"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What Node.js version is required for node:sqlite built-in module?

💡 Result:

The node:sqlite built-in module requires Node.js version 22.5.0 or later. It was added in v22.5.0 as an experimental feature (initially requiring the --experimental-sqlite flag). In later versions, the flag requirement was removed (v23.4.0, v22.13.0), and it progressed to release candidate status (v25.7.0).

Citations:


🏁 Script executed:

# Check package.json for engines field and the specific lines mentioned
head -80 package.json | tail -10

Repository: baanish/agent-render

Length of output: 255


🏁 Script executed:

# Search for node:sqlite usage in the codebase
rg "node:sqlite|SelfHostedArtifactStore" --type json --type ts --type js -A 2

Repository: baanish/agent-render

Length of output: 2372


🏁 Script executed:

# Check if server/selfhosted.ts exists and what it imports
fd "selfhosted.ts" -x cat -n {}

Repository: baanish/agent-render

Length of output: 1600


Update engines.node to match self-hosted Node.js requirement.

The self-hosted scripts depend on node:sqlite (used by SelfHostedArtifactStore), which requires Node.js 22.5.0 or later. However, package.json specifies engines.node: ">=20.10.0", allowing Node 20/21 where node:sqlite will fail at runtime. Either update engines.node to ">=22.5.0", or add a runtime check in server/selfhosted.ts to provide a helpful error message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 22 - 24, Update the Node engine constraint in
package.json or add a runtime guard in server/selfhosted.ts: either change the
package.json "engines.node" value to ">=22.5.0" so installs require a Node
version that supports node:sqlite, or add a startup check in
server/selfhosted.ts (before SelfHostedArtifactStore or any import/use of
node:sqlite) that inspects process.version and throws/logs a clear error
instructing users to run Node >=22.5.0; reference package.json::engines.node and
server/selfhosted.ts / SelfHostedArtifactStore / node:sqlite when making the
change.

Comment on lines +108 to +133
function renderMissingArtifactPage(reason: string): string {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Artifact unavailable · agent-render</title>
<style>
body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0b1020;color:#f5f7fb;margin:0;display:grid;min-height:100vh;place-items:center;padding:24px}
main{max-width:640px;background:#111a30;border:1px solid rgba(255,255,255,.12);border-radius:20px;padding:28px;box-shadow:0 20px 60px rgba(0,0,0,.35)}
h1{margin:0 0 12px;font-size:2rem}
p{line-height:1.7;color:#d7dcef}
code{background:rgba(255,255,255,.08);padding:.15rem .4rem;border-radius:.45rem}
a{color:#8dd4ff}
</style>
</head>
<body>
<main>
<h1>Artifact unavailable</h1>
<p>${reason}</p>
<p>Stored artifacts in self-hosted mode use a 24-hour sliding TTL. A successful read extends the expiry window; expired rows are removed on access or cleanup.</p>
<p><a href="/">Open the fragment-based homepage</a></p>
</main>
</body>
</html>`;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential XSS in error page rendering.

The reason parameter is interpolated directly into HTML at Line 127. While the current callers pass hardcoded strings, this could become a vulnerability if future code passes user-controlled input.

🛡️ Suggested fix to escape the reason string
+function escapeHtml(str: string): string {
+  return str
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;");
+}
+
 function renderMissingArtifactPage(reason: string): string {
   return `<!doctype html>
 ...
-      <p>${reason}</p>
+      <p>${escapeHtml(reason)}</p>
 ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/selfhosted-app.ts` around lines 108 - 133, The
renderMissingArtifactPage function currently interpolates the reason string
directly into HTML, risking XSS if caller input becomes untrusted; update
renderMissingArtifactPage to HTML-escape the reason before insertion (e.g.,
replace &, <, >, ", ' and / with safe entities) and use that escaped value in
the template; ensure the escaping utility is a small helper (escapeHtml or
similar) referenced and used inside renderMissingArtifactPage so all callers
(current and future) are protected.

Comment on lines +1 to +9
// @vitest-environment node
import os from "node:os";
import path from "node:path";
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { afterEach, describe, expect, it } from "vitest";
import { encodeEnvelope } from "@/lib/payload/fragment";
import type { PayloadEnvelope } from "@/lib/payload/schema";
import { createSelfHostedServer } from "../../server/selfhosted-app";

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the Node.js version requirement in package.json and CI workflow files
echo "=== package.json engines field ==="
cat package.json | jq '.engines'

echo -e "\n=== GitHub Actions workflow Node version ==="
fd -e yml -e yaml . .github/workflows --exec grep -A2 -E 'node-version|setup-node' {} \;

Repository: baanish/agent-render

Length of output: 269


🏁 Script executed:

#!/bin/bash
# Verify store.ts imports from node:sqlite
echo "=== Checking src/lib/selfhosted/store.ts for node:sqlite import ==="
fd -n "store.ts" src/ --exec cat {} \;

echo -e "\n=== Checking for SQLite-related dependencies in package.json ==="
cat package.json | jq '.dependencies, .devDependencies' | grep -i sqlite

echo -e "\n=== Checking full import chain in test file ==="
head -20 tests/selfhosted/server.test.ts

Repository: baanish/agent-render

Length of output: 1140


🏁 Script executed:

#!/bin/bash
# Find store.ts file properly
echo "=== Finding store.ts files ==="
find . -name "store.ts" -type f 2>/dev/null

echo -e "\n=== Searching for node:sqlite import anywhere in codebase ==="
rg "node:sqlite" --type ts --type js

echo -e "\n=== Searching for SelfHostedArtifactStore ==="
rg "SelfHostedArtifactStore" --type ts --type js -A 2 -B 2

echo -e "\n=== Checking what createSelfHostedServer imports/creates ==="
fd "selfhosted-app" . --exec cat {} \;

Repository: baanish/agent-render

Length of output: 11988


Pipeline failure: node:sqlite requires Node.js 22.5+

The CI error "No such built-in module: node:sqlite" indicates the test environment is running Node.js 20 (configured in your GitHub Actions workflow), but node:sqlite was introduced in Node.js 22.5.0. The SelfHostedArtifactStore in src/lib/selfhosted/store.ts imports DatabaseSync from node:sqlite.

Consider either:

  1. Updating the CI Node.js version to 22.5+ in your workflow configuration
  2. Using an alternative SQLite library like better-sqlite3 that works with older Node versions
🧰 Tools
🪛 GitHub Actions: Test

[error] 1-1: Vitest failed. Error: No such built-in module: node:sqlite

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/selfhosted/server.test.ts` around lines 1 - 9, The CI fails because
src/lib/selfhosted/store.ts imports DatabaseSync from the built-in node:sqlite
(requires Node 22.5+); update the implementation to use a Node 20-compatible
SQLite library instead (or make the import conditional). Replace the direct
import of DatabaseSync from "node:sqlite" in SelfHostedArtifactStore with a
wrapper that uses better-sqlite3 (or sqlite3) — e.g., swap the import for the
chosen package, adapt any DatabaseSync-typed usages in the
SelfHostedArtifactStore class and related functions to the API of the new
library (connection/open, prepare/run/get/all/close), and ensure tests and
exports that reference createSelfHostedServer continue to work with the new
store implementation; alternatively, update CI to Node 22.5+ if you prefer to
keep node:sqlite.

Comment on lines +1 to +9
// @vitest-environment node
import os from "node:os";
import path from "node:path";
import { mkdtempSync } from "node:fs";
import { afterEach, describe, expect, it } from "vitest";
import { encodeEnvelope } from "@/lib/payload/fragment";
import type { PayloadEnvelope } from "@/lib/payload/schema";
import { SelfHostedArtifactStore } from "@/lib/selfhosted/store";

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json engines =="
if [ -f package.json ]; then
  jq -r '.engines.node // "MISSING: engines.node"' package.json
else
  echo "MISSING: package.json"
fi

echo
echo "== workflow node-version configuration =="
if [ -d .github/workflows ]; then
  rg -n "actions/setup-node|node-version" .github/workflows -g "*.yml" -g "*.yaml" || true
else
  echo "MISSING: .github/workflows"
fi

echo
echo "== node:sqlite imports =="
rg -n "node:sqlite" || true

Repository: baanish/agent-render

Length of output: 612


🌐 Web query:

node:sqlite Node.js version requirement minimum support

💡 Result:

The built-in node:sqlite module requires Node.js v22.5.0 or later, as it was added in v22.5.0. It is currently at stability 1.2 (release candidate) in Node.js v25.x and still experimental in earlier versions like v22 and v23. Popular third-party SQLite libraries have these minimum Node.js requirements based on their documentation: - sqlite3 (now deprecated): Prebuilt binaries supported on Node v20.17.0+ (v6.x); earlier v5.x supported Node v10+. - better-sqlite3: Requires a currently supported Node.js version (as of 2026, Node 20 LTS+, 22 LTS+, 24 LTS+). Prebuilt binaries for LTS versions; an older fork mentioned v14.21.1+.

Citations:


Update Node.js version requirements to support node:sqlite (v22.5.0+).

The store code imports node:sqlite which requires Node.js v22.5.0 or later. Currently, engines.node in package.json is pinned to >=20.10.0 and GitHub Actions test workflow uses node-version: 20, both of which are below the minimum requirement. Update both to Node 20 LTS or later (recommended: 22.x LTS or 24.x LTS) to resolve CI failures.

🧰 Tools
🪛 GitHub Actions: Test

[error] 1-1: Vitest failed. Error: No such built-in module: node:sqlite

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/selfhosted/store.test.ts` around lines 1 - 9, Update the Node.js
requirement to meet node:sqlite's minimum (v22.5.0): change the package.json
engines.node entry (the "engines.node" field) to require >=22.5.0 (or a
recommended LTS range like ">=22.x" or ">=24.x"), and update the CI
configuration that sets the runner version (the workflow "node-version" key in
your GitHub Actions workflow) from 20 to a supported version (22 or 24) so tests
run with Node 22.5.0+.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant