Add Ubuntu onboard smoke flow and lazy-load auth startup
This commit is contained in:
29
Dockerfile.onboard-smoke
Normal file
29
Dockerfile.onboard-smoke
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ARG NODE_MAJOR=20
|
||||||
|
ARG PAPERCLIPAI_VERSION=latest
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
PAPERCLIP_HOME=/paperclip \
|
||||||
|
PAPERCLIP_OPEN_ON_LISTEN=false \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
|
PORT=3100 \
|
||||||
|
NODE_MAJOR=${NODE_MAJOR} \
|
||||||
|
PAPERCLIPAI_VERSION=${PAPERCLIPAI_VERSION}
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
|
||||||
|
> /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
VOLUME ["/paperclip"]
|
||||||
|
WORKDIR /workspace
|
||||||
|
EXPOSE 3100
|
||||||
|
|
||||||
|
CMD ["bash", "-lc", "set -euo pipefail; mkdir -p \"$PAPERCLIP_HOME\"; npx --yes \"paperclipai@${PAPERCLIPAI_VERSION}\" onboard --yes --data-dir \"$PAPERCLIP_HOME\""]
|
||||||
@@ -66,3 +66,29 @@ Notes:
|
|||||||
|
|
||||||
- Without API keys, the app still runs normally.
|
- Without API keys, the app still runs normally.
|
||||||
- Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites.
|
- Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites.
|
||||||
|
|
||||||
|
## Onboard Smoke Test (Ubuntu + npm only)
|
||||||
|
|
||||||
|
Use this when you want to mimic a fresh machine that only has Ubuntu + npm and verify:
|
||||||
|
|
||||||
|
- `npx paperclipai onboard --yes` completes
|
||||||
|
- the server binds to `0.0.0.0:3100` so host access works
|
||||||
|
|
||||||
|
Build + run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/docker-onboard-smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Open: `http://localhost:3100`
|
||||||
|
|
||||||
|
Useful overrides:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
HOST_PORT=3200 PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Persistent data is mounted at `./data/docker-onboard-smoke` by default.
|
||||||
|
- The image definition is in `Dockerfile.onboard-smoke`.
|
||||||
|
|||||||
28
scripts/docker-onboard-smoke.sh
Executable file
28
scripts/docker-onboard-smoke.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
IMAGE_NAME="${IMAGE_NAME:-paperclip-onboard-smoke}"
|
||||||
|
HOST_PORT="${HOST_PORT:-3100}"
|
||||||
|
PAPERCLIPAI_VERSION="${PAPERCLIPAI_VERSION:-latest}"
|
||||||
|
DATA_DIR="${DATA_DIR:-$REPO_ROOT/data/docker-onboard-smoke}"
|
||||||
|
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
|
echo "==> Building onboard smoke image"
|
||||||
|
docker build \
|
||||||
|
--build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \
|
||||||
|
-f "$REPO_ROOT/Dockerfile.onboard-smoke" \
|
||||||
|
-t "$IMAGE_NAME" \
|
||||||
|
"$REPO_ROOT"
|
||||||
|
|
||||||
|
echo "==> Running onboard smoke container"
|
||||||
|
echo " UI should be reachable at: http://localhost:$HOST_PORT"
|
||||||
|
echo " Data dir: $DATA_DIR"
|
||||||
|
docker run --rm \
|
||||||
|
--name "${IMAGE_NAME//[^a-zA-Z0-9_.-]/-}" \
|
||||||
|
-p "$HOST_PORT:3100" \
|
||||||
|
-e HOST=0.0.0.0 \
|
||||||
|
-e PORT=3100 \
|
||||||
|
-v "$DATA_DIR:/paperclip" \
|
||||||
|
"$IMAGE_NAME"
|
||||||
@@ -4,7 +4,7 @@ import { createServer } from "node:http";
|
|||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { createInterface } from "node:readline/promises";
|
import { createInterface } from "node:readline/promises";
|
||||||
import { stdin, stdout } from "node:process";
|
import { stdin, stdout } from "node:process";
|
||||||
import type { Request as ExpressRequest } from "express";
|
import type { Request as ExpressRequest, RequestHandler } from "express";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
createDb,
|
createDb,
|
||||||
@@ -26,12 +26,17 @@ import { heartbeatService } from "./services/index.js";
|
|||||||
import { createStorageServiceFromConfig } from "./storage/index.js";
|
import { createStorageServiceFromConfig } from "./storage/index.js";
|
||||||
import { printStartupBanner } from "./startup-banner.js";
|
import { printStartupBanner } from "./startup-banner.js";
|
||||||
import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js";
|
import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js";
|
||||||
import {
|
|
||||||
createBetterAuthHandler,
|
type BetterAuthSessionUser = {
|
||||||
createBetterAuthInstance,
|
id: string;
|
||||||
resolveBetterAuthSession,
|
email?: string | null;
|
||||||
resolveBetterAuthSessionFromHeaders,
|
name?: string | null;
|
||||||
} from "./auth/better-auth.js";
|
};
|
||||||
|
|
||||||
|
type BetterAuthSessionResult = {
|
||||||
|
session: { id: string; userId: string } | null;
|
||||||
|
user: BetterAuthSessionUser | null;
|
||||||
|
};
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
type EmbeddedPostgresInstance = {
|
||||||
initialise(): Promise<void>;
|
initialise(): Promise<void>;
|
||||||
@@ -388,17 +393,23 @@ if (config.deploymentMode === "authenticated") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let authReady = config.deploymentMode === "local_trusted";
|
let authReady = config.deploymentMode === "local_trusted";
|
||||||
let betterAuthHandler: ReturnType<typeof createBetterAuthHandler> | undefined;
|
let betterAuthHandler: RequestHandler | undefined;
|
||||||
let resolveSession:
|
let resolveSession:
|
||||||
| ((req: ExpressRequest) => Promise<Awaited<ReturnType<typeof resolveBetterAuthSession>>>)
|
| ((req: ExpressRequest) => Promise<BetterAuthSessionResult | null>)
|
||||||
| undefined;
|
| undefined;
|
||||||
let resolveSessionFromHeaders:
|
let resolveSessionFromHeaders:
|
||||||
| ((headers: Headers) => Promise<Awaited<ReturnType<typeof resolveBetterAuthSession>>>)
|
| ((headers: Headers) => Promise<BetterAuthSessionResult | null>)
|
||||||
| undefined;
|
| undefined;
|
||||||
if (config.deploymentMode === "local_trusted") {
|
if (config.deploymentMode === "local_trusted") {
|
||||||
await ensureLocalTrustedBoardPrincipal(db as any);
|
await ensureLocalTrustedBoardPrincipal(db as any);
|
||||||
}
|
}
|
||||||
if (config.deploymentMode === "authenticated") {
|
if (config.deploymentMode === "authenticated") {
|
||||||
|
const {
|
||||||
|
createBetterAuthHandler,
|
||||||
|
createBetterAuthInstance,
|
||||||
|
resolveBetterAuthSession,
|
||||||
|
resolveBetterAuthSessionFromHeaders,
|
||||||
|
} = await import("./auth/better-auth.js");
|
||||||
const betterAuthSecret =
|
const betterAuthSecret =
|
||||||
process.env.BETTER_AUTH_SECRET?.trim() ?? process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim();
|
process.env.BETTER_AUTH_SECRET?.trim() ?? process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim();
|
||||||
if (!betterAuthSecret) {
|
if (!betterAuthSecret) {
|
||||||
|
|||||||
Reference in New Issue
Block a user