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}