Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions dashboard/next.config.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions dashboard/src/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions dashboard/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
24 changes: 22 additions & 2 deletions dashboard/src/lib/server/kb-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,17 @@ async function doFetch(path: string, init: RequestInit): Promise<Response> {
}

async function callInternal(path: string, payload: Record<string, unknown>, ctx: ActorContext): Promise<Record<string, unknown>> {
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: {
Expand Down Expand Up @@ -155,7 +165,17 @@ export async function callInternalMultipart(
file: { filename: string; content: Uint8Array; contentType: string },
ctx: ActorContext,
): Promise<Record<string, unknown>> {
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);
Expand Down
43 changes: 35 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/backend/runbooks/local-dev-and-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
62 changes: 62 additions & 0 deletions infra/caddy/Caddyfile
Original file line number Diff line number Diff line change
@@ -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
}
34 changes: 34 additions & 0 deletions infra/caddy/README.md
Original file line number Diff line number Diff line change
@@ -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://<CADDY_PRIMARY_HOST>/` -> dashboard (`resolvekit_dashboard:3000`)
- `https://<CADDY_PRIMARY_HOST>/v1/*` -> dashboard API (`resolvekit_api:3002`)
- `https://<CADDY_PRIMARY_HOST>/agent/*` -> backend runtime (`resolvekit_backend:8000`)
- `https://<CADDY_PRIMARY_HOST>/kb/*` -> KB service (`resolvekit_kb_service:8100`)
- `https://<CADDY_API_HOST>/` -> 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`).
1 change: 1 addition & 0 deletions infra/caddy/config/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions infra/caddy/data/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

25 changes: 25 additions & 0 deletions infra/caddy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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}