diff --git a/.env.example b/.env.example
index ac60423..71967e4 100644
--- a/.env.example
+++ b/.env.example
@@ -24,6 +24,8 @@ IAA_WS_OWNER_TTL_SECONDS=120
IAA_WS_OUTBOX_TTL_SECONDS=300
IAA_WS_TOOL_RESULT_TTL_SECONDS=300
IAA_KNOWLEDGE_BASES_BASE_URL=http://kb-service:8100
+# Required. Must be a random secret shared with KBS_SERVICE_JWT_SIGNING_KEY.
+# If left as the placeholder value, dashboard KB endpoints will fail.
IAA_KNOWLEDGE_BASES_SIGNING_KEY=change-me-kb-service-signing-key
IAA_KNOWLEDGE_BASES_AUDIENCE=kb-service
IAA_KNOWLEDGE_BASES_JWT_ALGORITHM=HS256
@@ -42,6 +44,7 @@ DATABASE_URL=postgresql://agent:postgres@db:5432/agent
# Knowledge base service
KBS_DATABASE_URL=postgresql+asyncpg://agent:postgres@kb-db:5432/knowledge_bases
KBS_POSTGRES_DB=knowledge_bases
+# Required. Must match IAA_KNOWLEDGE_BASES_SIGNING_KEY.
KBS_SERVICE_JWT_SIGNING_KEY=change-me-kb-service-signing-key
KBS_SERVICE_JWT_AUDIENCE=kb-service
KBS_ENCRYPTION_KEY=change-me-generate-with-python-fernet
@@ -62,3 +65,23 @@ GATEWAY_HTTPS_BIND_ADDR=0.0.0.0
GATEWAY_HTTPS_BIND_PORT=443
RESOLVEKIT_PUBLIC_HOST=support.example.com
LETSENCRYPT_EMAIL=devops@example.com
+
+# Optional: dedicated Dockerized Caddy gateway (`infra/caddy`).
+# This mode supports one main host + explicit `www`/`dash` + `api` hosts,
+# while keeping path routing (`/agent/*`, `/v1/*`) available on the main host.
+CADDY_DOCKER_NETWORK=resolvekit_default
+CADDY_HTTP_BIND=0.0.0.0:80
+CADDY_HTTPS_BIND=0.0.0.0:443
+CADDY_LOCAL_BIND=127.0.0.1:8080
+
+# Public domains served by infra/caddy/Caddyfile
+CADDY_PRIMARY_HOST=support.example.com
+CADDY_WWW_HOST=www.support.example.com
+CADDY_DASH_HOST=dash.support.example.com
+CADDY_API_HOST=api.support.example.com
+
+# Internal upstream targets (override only if your service/container names differ)
+CADDY_DASHBOARD_UPSTREAM=resolvekit_dashboard:3000
+CADDY_API_UPSTREAM=resolvekit_api:3002
+CADDY_BACKEND_UPSTREAM=resolvekit_backend:8000
+CADDY_KB_UPSTREAM=resolvekit_kb_service:8100
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1867cf3..f050c24 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,6 +52,16 @@ jobs:
docker compose -f docker-compose.prod.yml config -q
docker compose -f docker-compose.local-deploy.yml config -q
+ agent-docs:
+ name: Agent Docs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v5
+
+ - name: Validate AGENTS documentation exists
+ run: test -f AGENTS.md
+
dashboard:
name: Dashboard
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 89df5b6..8266786 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,12 @@ ResolveKit Backend provides the server-side runtime for embedded app assistants.
- `cp .env.local-deploy.example .env.local-deploy`
- configure your public hostname + Let's Encrypt values in `.env.local-deploy`
- `docker compose -f docker-compose.local-deploy.yml --env-file .env --env-file .env.local-deploy up -d --build`
+5. Optional standalone Dockerized Caddy gateway:
+ - configure `CADDY_*` domain/bind values in `.env`
+ - `docker compose -f infra/caddy/docker-compose.yml up -d`
+6. Optional Caddy from main compose (recommended if you want one command family):
+ - configure `CADDY_*` values in `.env`
+ - `docker compose --profile gateway up -d`
## Deployment Modes
diff --git a/dashboard/next.config.ts b/dashboard/next.config.ts
index ce7062b..fc681b2 100644
--- a/dashboard/next.config.ts
+++ b/dashboard/next.config.ts
@@ -1,10 +1,31 @@
import type { NextConfig } from "next";
+const allowedDevOrigins = Array.from(
+ new Set(
+ [
+ process.env.CADDY_PRIMARY_HOST,
+ process.env.CADDY_WWW_HOST,
+ process.env.CADDY_DASH_HOST,
+ process.env.CADDY_API_HOST,
+ process.env.RESOLVEKIT_PUBLIC_HOST,
+ process.env.RESOLVEKIT_CONSOLE_HOST,
+ process.env.RESOLVEKIT_API_HOST,
+ process.env.RESOLVEKIT_AGENT_HOST,
+ process.env.NEXT_ALLOWED_DEV_ORIGINS,
+ ]
+ .filter(Boolean)
+ .flatMap((value) => String(value).split(","))
+ .map((value) => value.trim())
+ .filter(Boolean),
+ ),
+);
+
const nextConfig: NextConfig = {
reactStrictMode: false,
// Dashboard and API run separate Next dev processes from the same source tree.
// Allow overriding distDir so they do not contend on a shared .next cache.
distDir: process.env.NEXT_DIST_DIR ?? ".next",
+ ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}),
};
export default nextConfig;
diff --git a/dashboard/src/app/icon.svg b/dashboard/src/app/icon.svg
new file mode 100644
index 0000000..71f7c29
--- /dev/null
+++ b/dashboard/src/app/icon.svg
@@ -0,0 +1,13 @@
+
diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx
index 17a1d21..558193a 100644
--- a/dashboard/src/app/layout.tsx
+++ b/dashboard/src/app/layout.tsx
@@ -5,6 +5,11 @@ import "./globals.css";
export const metadata: Metadata = {
title: "ResolveKit Dashboard",
description: "ResolveKit control plane",
+ icons: {
+ icon: "/icon.svg",
+ shortcut: "/icon.svg",
+ apple: "/icon.svg",
+ },
};
export default function RootLayout({
diff --git a/dashboard/src/lib/server/kb-service.ts b/dashboard/src/lib/server/kb-service.ts
index cc2c5d5..458d1a6 100644
--- a/dashboard/src/lib/server/kb-service.ts
+++ b/dashboard/src/lib/server/kb-service.ts
@@ -125,7 +125,17 @@ async function doFetch(path: string, init: RequestInit): Promise {
}
async function callInternal(path: string, payload: Record, ctx: ActorContext): Promise> {
- const token = await buildServiceToken(ctx);
+ let token: string;
+ try {
+ token = await buildServiceToken(ctx);
+ } catch (error) {
+ console.error("KB token generation failed", error);
+ throw new KBServiceError({
+ status: 500,
+ detail: "Knowledge base integration is misconfigured",
+ code: "kb_auth_misconfigured",
+ });
+ }
const response = await doFetch(path, {
method: "POST",
headers: {
@@ -155,7 +165,17 @@ export async function callInternalMultipart(
file: { filename: string; content: Uint8Array; contentType: string },
ctx: ActorContext,
): Promise> {
- const token = await buildServiceToken(ctx);
+ let token: string;
+ try {
+ token = await buildServiceToken(ctx);
+ } catch (error) {
+ console.error("KB token generation failed", error);
+ throw new KBServiceError({
+ status: 500,
+ detail: "Knowledge base integration is misconfigured",
+ code: "kb_auth_misconfigured",
+ });
+ }
const form = new FormData();
for (const [key, value] of Object.entries(fields)) {
form.set(key, value);
diff --git a/docker-compose.yml b/docker-compose.yml
index 3f6ff02..84a57a1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -109,14 +109,16 @@ services:
working_dir: /app
depends_on:
- api
+ env_file: .env
command: >
- sh -c "rm -rf .next-dashboard &&
- npm install --legacy-peer-deps --no-audit --no-fund --package-lock=false &&
- npx prisma generate &&
+ sh -lc "set -e;
+ rm -rf .next-dashboard;
+ npm install --legacy-peer-deps --no-audit --no-fund --package-lock=false;
+ npx prisma generate;
node -e \"const net=require('net'); const close=(a,b)=>()=>{a.destroy(); b.destroy();}; net.createServer((client)=>{ const upstream=net.connect(8000,'backend'); client.pipe(upstream); upstream.pipe(client); client.on('error', close(client, upstream)); upstream.on('error', close(upstream, client)); }).listen(8000,'127.0.0.1');\" &
npm run dev -- --hostname 0.0.0.0 --port 3000"
environment:
- NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3002}
+ NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-}
RESOLVEKIT_SERVER_AGENT_BASE_URL: ${RESOLVEKIT_SERVER_AGENT_BASE_URL:-http://localhost:8000}
DATABASE_URL: postgresql://${POSTGRES_USER:-agent}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-agent}
IAA_JWT_SECRET: ${IAA_JWT_SECRET:-change-me-generate-with-openssl-rand-hex-32}
@@ -145,14 +147,16 @@ services:
working_dir: /app
depends_on:
- backend
+ env_file: .env
command: >
- sh -c "rm -rf .next-api &&
- npm install --legacy-peer-deps --no-audit --no-fund --package-lock=false &&
- npx prisma generate &&
+ sh -lc "set -e;
+ rm -rf .next-api;
+ npm install --legacy-peer-deps --no-audit --no-fund --package-lock=false;
+ npx prisma generate;
node -e \"const net=require('net'); const close=(a,b)=>()=>{a.destroy(); b.destroy();}; net.createServer((client)=>{ const upstream=net.connect(8000,'backend'); client.pipe(upstream); upstream.pipe(client); client.on('error', close(client, upstream)); upstream.on('error', close(upstream, client)); }).listen(8000,'127.0.0.1');\" &
npm run dev -- --hostname 0.0.0.0 --port 3002"
environment:
- NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:3002}
+ NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-}
RESOLVEKIT_SERVER_AGENT_BASE_URL: ${RESOLVEKIT_SERVER_AGENT_BASE_URL:-http://localhost:8000}
DATABASE_URL: postgresql://${POSTGRES_USER:-agent}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-agent}
IAA_JWT_SECRET: ${IAA_JWT_SECRET:-change-me-generate-with-openssl-rand-hex-32}
@@ -175,6 +179,29 @@ services:
networks:
- resolvekit
+ caddy:
+ image: caddy:2.8.4
+ container_name: resolvekit_caddy
+ restart: unless-stopped
+ profiles: ["gateway"]
+ depends_on:
+ - backend
+ - kb-service
+ - dashboard
+ - api
+ env_file: .env
+ ports:
+ - "${CADDY_HTTP_BIND:-0.0.0.0:80}:80"
+ - "${CADDY_HTTPS_BIND:-0.0.0.0:443}:443"
+ - "${CADDY_HTTPS_BIND:-0.0.0.0:443}:443/udp"
+ - "${CADDY_LOCAL_BIND:-127.0.0.1:8080}:8080"
+ volumes:
+ - ./infra/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
+ - ./infra/caddy/data:/data
+ - ./infra/caddy/config:/config
+ networks:
+ - resolvekit
+
networks:
resolvekit:
name: resolvekit_default
diff --git a/docs/backend/runbooks/local-dev-and-docker.md b/docs/backend/runbooks/local-dev-and-docker.md
index b4c7e56..a011375 100644
--- a/docs/backend/runbooks/local-dev-and-docker.md
+++ b/docs/backend/runbooks/local-dev-and-docker.md
@@ -9,6 +9,20 @@
## Local deploy notes
+### Optional Dockerized Caddy gateway
+
+If you want Caddy separate from the main compose stack, use `infra/caddy`:
+
+- configure `CADDY_PRIMARY_HOST`, `CADDY_WWW_HOST`, `CADDY_DASH_HOST`, `CADDY_API_HOST`, and `LETSENCRYPT_EMAIL` in `.env`
+- start gateway: `docker compose -f infra/caddy/docker-compose.yml up -d`
+- this gateway reads `.env` directly and proxies to the existing local Docker services on `resolvekit_default`
+
+If you prefer keeping everything in the main compose file, use:
+
+- `docker compose --profile gateway up -d`
+
+This starts the same Caddy config via the `caddy` service in `docker-compose.yml`.
+
### Single-host quickstart
The local deploy templates are optimized for one public host:
diff --git a/infra/caddy/Caddyfile b/infra/caddy/Caddyfile
new file mode 100644
index 0000000..39b9570
--- /dev/null
+++ b/infra/caddy/Caddyfile
@@ -0,0 +1,62 @@
+{
+ email {$LETSENCRYPT_EMAIL}
+ servers {
+ # Keep original client IP/proto when running behind another private edge.
+ trusted_proxies static private_ranges
+ }
+}
+
+(resolvekit_routes) {
+ encode zstd gzip
+
+ @agent path /agent /agent/*
+ handle @agent {
+ uri strip_prefix /agent
+ reverse_proxy {$CADDY_BACKEND_UPSTREAM:resolvekit_backend:8000} {
+ flush_interval -1
+ transport http {
+ read_timeout 0
+ write_timeout 0
+ }
+ }
+ }
+
+ @kb path /kb /kb/*
+ handle @kb {
+ uri strip_prefix /kb
+ reverse_proxy {$CADDY_KB_UPSTREAM:resolvekit_kb_service:8100}
+ }
+
+ @api_v1 path /v1 /v1/*
+ handle @api_v1 {
+ reverse_proxy {$CADDY_API_UPSTREAM:resolvekit_api:3002}
+ }
+
+ @api_alias path /api /api/*
+ handle @api_alias {
+ uri strip_prefix /api
+ reverse_proxy {$CADDY_API_UPSTREAM:resolvekit_api:3002}
+ }
+
+ handle {
+ reverse_proxy {$CADDY_DASHBOARD_UPSTREAM:resolvekit_dashboard:3000}
+ }
+}
+
+{$CADDY_PRIMARY_HOST:support.example.com}, {$CADDY_WWW_HOST:www.support.example.com}, {$CADDY_DASH_HOST:dash.support.example.com} {
+ import resolvekit_routes
+}
+
+{$CADDY_API_HOST:api.support.example.com} {
+ reverse_proxy {$CADDY_BACKEND_UPSTREAM:resolvekit_backend:8000} {
+ flush_interval -1
+ transport http {
+ read_timeout 0
+ write_timeout 0
+ }
+ }
+}
+
+:8080 {
+ import resolvekit_routes
+}
diff --git a/infra/caddy/README.md b/infra/caddy/README.md
new file mode 100644
index 0000000..4eeb97b
--- /dev/null
+++ b/infra/caddy/README.md
@@ -0,0 +1,34 @@
+# Caddy Gateway (Docker)
+
+This stack runs Caddy as an external reverse proxy for the local OSS Docker services.
+
+## Start
+
+1. Ensure backend stack is up:
+ - `docker compose up -d`
+2. Configure domains in root `.env`:
+ - `CADDY_PRIMARY_HOST`
+ - `CADDY_WWW_HOST`
+ - `CADDY_DASH_HOST`
+ - `CADDY_API_HOST`
+ - `LETSENCRYPT_EMAIL`
+3. Start Caddy:
+ - `docker compose -f infra/caddy/docker-compose.yml up -d`
+
+## Routing
+
+- `https:///` -> dashboard (`resolvekit_dashboard:3000`)
+- `https:///v1/*` -> dashboard API (`resolvekit_api:3002`)
+- `https:///agent/*` -> backend runtime (`resolvekit_backend:8000`)
+- `https:///kb/*` -> KB service (`resolvekit_kb_service:8100`)
+- `https:///` -> backend runtime (`resolvekit_backend:8000`)
+
+`/api/*` is also supported as an alias for `/v1/*`.
+
+## Notes
+
+- `CADDY_DOCKER_NETWORK` defaults to `resolvekit_default` (the network from `docker-compose.yml`).
+- If another edge proxy already owns public `:80/:443`, set:
+ - `CADDY_HTTP_BIND=127.0.0.1:18080`
+ - `CADDY_HTTPS_BIND=127.0.0.1:18443`
+- Keep `CADDY_LOCAL_BIND` on loopback for tailnet or local-only access (default `127.0.0.1:8080`).
diff --git a/infra/caddy/config/.gitkeep b/infra/caddy/config/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/infra/caddy/config/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/infra/caddy/data/.gitkeep b/infra/caddy/data/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/infra/caddy/data/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/infra/caddy/docker-compose.yml b/infra/caddy/docker-compose.yml
new file mode 100644
index 0000000..9e7a642
--- /dev/null
+++ b/infra/caddy/docker-compose.yml
@@ -0,0 +1,25 @@
+name: resolvekit-caddy
+
+services:
+ caddy:
+ image: caddy:2.8.4
+ container_name: resolvekit_caddy
+ restart: unless-stopped
+ env_file:
+ - ../../.env
+ ports:
+ - "${CADDY_HTTP_BIND:-0.0.0.0:80}:80"
+ - "${CADDY_HTTPS_BIND:-0.0.0.0:443}:443"
+ - "${CADDY_HTTPS_BIND:-0.0.0.0:443}:443/udp"
+ - "${CADDY_LOCAL_BIND:-127.0.0.1:8080}:8080"
+ volumes:
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
+ - ./data:/data
+ - ./config:/config
+ networks:
+ - resolvekit
+
+networks:
+ resolvekit:
+ external: true
+ name: ${CADDY_DOCKER_NETWORK:-resolvekit_default}