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
15 changes: 15 additions & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ COPY --from=deps /app/ .
COPY --from=pruner /app/out/full/ .
RUN pnpm build --filter=@clawe/web

# Deploy cloud-plugins with resolved deps (cloud edition only)
RUN if [ "$NEXT_PUBLIC_CLAWE_EDITION" = "cloud" ]; then \
pnpm --filter=@clawe/cloud-plugins deploy /app/cloud-plugins-deploy --prod; \
fi

# Production runner
FROM node:22-alpine AS runner
WORKDIR /app
Expand All @@ -52,6 +57,16 @@ COPY --from=builder --chown=node:node /app/apps/web/.next/standalone ./
COPY --from=builder --chown=node:node /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=node:node /app/apps/web/public ./apps/web/public

# Cloud-plugins runtime deps (cloud edition only)
ARG NEXT_PUBLIC_CLAWE_EDITION
RUN --mount=from=builder,source=/app,target=/tmp/builder \
if [ "$NEXT_PUBLIC_CLAWE_EDITION" = "cloud" ]; then \
mkdir -p ./node_modules/@clawe/cloud-plugins && \
cp /tmp/builder/cloud-plugins-deploy/package.json ./node_modules/@clawe/cloud-plugins/ && \
cp -r /tmp/builder/cloud-plugins-deploy/dist ./node_modules/@clawe/cloud-plugins/ && \
cp -r /tmp/builder/cloud-plugins-deploy/node_modules/. ./node_modules/; \
fi

USER node

EXPOSE 3000
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState } from "react";
import { useQuery, useMutation as useConvexMutation } from "convex/react";
import { useMutation } from "@tanstack/react-query";
import { api } from "@clawe/backend";
Expand All @@ -11,7 +11,6 @@ import { Spinner } from "@clawe/ui/components/spinner";
import { Skeleton } from "@clawe/ui/components/skeleton";
import { CheckCircle2, Eye, EyeOff, Pencil } from "lucide-react";
import { toast } from "sonner";
import { loadPlugins, hasPlugin } from "@clawe/plugins";
import { patchApiKeys } from "@/lib/squadhub/actions";
import { useApiClient } from "@/hooks/use-api-client";
import { config } from "@/lib/config";
Expand Down Expand Up @@ -182,13 +181,7 @@ export const ApiKeysSettings = () => {
const [openaiKey, setOpenaiKey] = useState("");
const [anthropicValid, setAnthropicValid] = useState<boolean | null>(null);
const [openaiValid, setOpenaiValid] = useState<boolean | null>(null);
const [isCloud, setIsCloud] = useState(config.isCloud);

useEffect(() => {
loadPlugins().then(() => {
setIsCloud(hasPlugin());
});
}, []);
const isCloud = config.isCloud;

const anthropicValidation = useMutation({
mutationFn: async (apiKey: string) => {
Expand Down
22 changes: 20 additions & 2 deletions apps/web/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
import { handlers } from "@/lib/auth/nextauth-config";
import { type NextRequest, NextResponse } from "next/server";
import { config } from "@/lib/config";

export const { GET, POST } = handlers;
const notAvailable = () =>
NextResponse.json({ error: "Not available" }, { status: 404 });

const isCognito = config.authProvider === "cognito";

export const GET = isCognito
? notAvailable
: async (request: NextRequest) => {
const { handlers } = await import("@/lib/auth/nextauth-config");
return handlers.GET(request);
};

export const POST = isCognito
? notAvailable
: async (request: NextRequest) => {
const { handlers } = await import("@/lib/auth/nextauth-config");
return handlers.POST(request);
};
5 changes: 2 additions & 3 deletions apps/web/src/app/api/squadhub/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { checkHealth } from "@clawe/shared/squadhub";
import { loadPlugins, getPlugin } from "@clawe/plugins";
import { resolvePlugin } from "@/lib/plugins";
import { getAuthenticatedTenant } from "@/lib/api/tenant-auth";
import { getConnection } from "@/lib/squadhub/connection";
import { config } from "@/lib/config";
Expand All @@ -19,8 +19,7 @@ async function isInfraRunning(
): Promise<boolean> {
try {
if (config.isCloud) {
await loadPlugins();
const lifecycle = getPlugin("squadhub-lifecycle");
const lifecycle = await resolvePlugin("squadhub-lifecycle");
const status = await lifecycle.getStatus(tenantId);
return status.running;
}
Expand Down
59 changes: 56 additions & 3 deletions apps/web/src/app/api/tenant/provision/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { ConvexHttpClient } from "convex/browser";
import { api } from "@clawe/backend";
import { loadPlugins, getPlugin } from "@clawe/plugins";
import { resolvePlugin } from "@/lib/plugins";
import { setupTenant } from "@/lib/squadhub/setup";
import { patchApiKeys } from "@/lib/squadhub/actions";
import { logger as baseLogger } from "@/lib/logger";

const logger = baseLogger.child({ route: "tenant/provision" });

/**
* POST /api/tenant/provision
Expand Down Expand Up @@ -51,32 +54,51 @@ export const POST = async (request: NextRequest) => {

try {
// 2. Ensure account exists
logger.info("Ensuring account exists");
const account = await convex.mutation(api.accounts.getOrCreateForUser, {});
logger.info({ accountId: account._id }, "Account ready");

// 3. Check for existing tenant
const existingTenant = await convex.query(
api.tenants.getForCurrentUser,
{},
);
logger.info(
{
hasTenant: !!existingTenant,
status: existingTenant?.status,
tenantId: existingTenant?._id,
},
"Existing tenant check",
);

if (existingTenant && existingTenant.status === "active") {
// Tenant already provisioned — just re-run app setup below
logger.info("Tenant already active, skipping provisioning");
} else {
// 4. Create tenant + provision via plugin
await loadPlugins();
const provisioner = getPlugin("squadhub-provisioner");
const provisioner = await resolvePlugin("squadhub-provisioner");

// Create tenant record (or use existing non-active one)
const tenantIdToProvision = existingTenant
? existingTenant._id
: await convex.mutation(api.tenants.create, {});
logger.info({ tenantId: tenantIdToProvision }, "Provisioning tenant");

// Provision infrastructure (dev: reads env vars)
const provisionResult = await provisioner.provision({
tenantId: tenantIdToProvision,
accountId: account._id,
convexUrl,
});
logger.info(
{
squadhubUrl: provisionResult.squadhubUrl,
hasToken: !!provisionResult.squadhubToken,
metadata: provisionResult.metadata,
},
"Provision result",
);

// Update tenant with connection details
await convex.mutation(api.tenants.updateStatus, {
Expand All @@ -90,22 +112,41 @@ export const POST = async (request: NextRequest) => {
efsAccessPointId: provisionResult.metadata.efsAccessPointId,
}),
});
logger.info("Tenant status updated to active");
}

// Re-fetch tenant to get latest connection details
const tenant = await convex.query(api.tenants.getForCurrentUser, {});
logger.info(
{
tenantId: tenant?._id,
status: tenant?.status,
hasSquadhubUrl: !!tenant?.squadhubUrl,
hasSquadhubToken: !!tenant?.squadhubToken,
},
"Re-fetched tenant",
);

if (!tenant) {
logger.error("Tenant not found after provisioning");
return NextResponse.json(
{ error: "Failed to retrieve tenant after provisioning" },
{ status: 500 },
);
} else if (tenant.status !== "active") {
logger.error({ status: tenant.status }, "Tenant in unexpected status");
return NextResponse.json(
{ error: `Tenant in unexpected status "${tenant.status}"` },
{ status: 500 },
);
} else if (!tenant.squadhubUrl || !tenant.squadhubToken) {
logger.error(
{
squadhubUrl: tenant.squadhubUrl ?? null,
hasToken: !!tenant.squadhubToken,
},
"Tenant missing Squadhub connection details",
);
return NextResponse.json(
{ error: "Tenant missing Squadhub connection details" },
{ status: 500 },
Expand All @@ -124,10 +165,21 @@ export const POST = async (request: NextRequest) => {
tenant.openaiApiKey ?? undefined,
connection,
);
logger.info("API keys patched");
}

// 6. Run app-level setup (agents, crons, routines)
logger.info("Running app-level setup");
const result = await setupTenant(connection, convexUrl, authToken);
logger.info(
{
agents: result.agents,
crons: result.crons,
routines: result.routines,
errors: result.errors,
},
"Setup complete",
);

// 7. Return result
return NextResponse.json({
Expand All @@ -140,6 +192,7 @@ export const POST = async (request: NextRequest) => {
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
logger.error({ err: error }, "Provision failed");
return NextResponse.json({ error: message }, { status: 500 });
}
};
5 changes: 2 additions & 3 deletions apps/web/src/app/api/tenant/squadhub/restart/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { loadPlugins, getPlugin } from "@clawe/plugins";
import { resolvePlugin } from "@/lib/plugins";
import { getAuthenticatedTenant } from "@/lib/api/tenant-auth";

/**
Expand All @@ -14,8 +14,7 @@ export const POST = async (request: NextRequest) => {
if (error) return error;

try {
await loadPlugins();
const lifecycle = getPlugin("squadhub-lifecycle");
const lifecycle = await resolvePlugin("squadhub-lifecycle");
await lifecycle.restart(tenant._id);

return NextResponse.json({ ok: true });
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/tenant/squadhub/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { loadPlugins, getPlugin } from "@clawe/plugins";
import { resolvePlugin } from "@/lib/plugins";
import { getAuthenticatedTenant } from "@/lib/api/tenant-auth";

/**
Expand All @@ -14,8 +14,7 @@ export const DELETE = async (request: NextRequest) => {
if (error) return error;

try {
await loadPlugins();
const lifecycle = getPlugin("squadhub-lifecycle");
const lifecycle = await resolvePlugin("squadhub-lifecycle");
await lifecycle.destroy(tenant._id);

return NextResponse.json({ ok: true });
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/tenant/squadhub/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { loadPlugins, getPlugin } from "@clawe/plugins";
import { resolvePlugin } from "@/lib/plugins";
import { getAuthenticatedTenant } from "@/lib/api/tenant-auth";

/**
Expand All @@ -15,8 +15,7 @@ export const GET = async (request: NextRequest) => {
if (error) return error;

try {
await loadPlugins();
const lifecycle = getPlugin("squadhub-lifecycle");
const lifecycle = await resolvePlugin("squadhub-lifecycle");
const status = await lifecycle.getStatus(tenant._id);

return NextResponse.json(status);
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/api/tenant/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { loadPlugins, getPlugin } from "@clawe/plugins";
import { resolvePlugin } from "@/lib/plugins";
import { getAuthenticatedTenant } from "@/lib/api/tenant-auth";

/**
Expand All @@ -15,8 +15,7 @@ export const GET = async (request: NextRequest) => {
if (error) return error;

try {
await loadPlugins();
const provisioner = getPlugin("squadhub-provisioner");
const provisioner = await resolvePlugin("squadhub-provisioner");
const status = await provisioner.getProvisioningStatus(tenant._id);

return NextResponse.json(status);
Expand Down
19 changes: 19 additions & 0 deletions apps/web/src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pino from "pino";

const isDev = process.env.NODE_ENV !== "production";

export const logger = pino({
level: process.env.LOG_LEVEL || "info",
name: "web",
formatters: {
level: (label) => ({ level: label }),
},
...(isDev && {
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
}),
});
28 changes: 28 additions & 0 deletions apps/web/src/lib/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getPlugin, hasPlugin, registerPlugins } from "@clawe/plugins";
import type { PluginMap, CloudPluginRegister } from "@clawe/plugins";
import { config } from "@/lib/config";
import { logger } from "@/lib/logger";

const CLOUD_PLUGINS_PKG = "@clawe/cloud-plugins";

let loading: Promise<void> | undefined;

async function ensurePlugins(): Promise<void> {
if (hasPlugin() || !config.isCloud) return;
if (!loading) {
loading = (async () => {
const mod: { register: CloudPluginRegister } = await import(
/* webpackIgnore: true */ CLOUD_PLUGINS_PKG
);
registerPlugins(mod.register(logger), logger);
})();
}
await loading;
}

export async function resolvePlugin<K extends keyof PluginMap>(
name: K,
): Promise<PluginMap[K]> {
await ensurePlugins();
return getPlugin(name);
}
9 changes: 0 additions & 9 deletions packages/plugins/src/cloud-plugins.d.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/plugins/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Registry
export { loadPlugins, hasPlugin, getPlugin } from "./registry";
export type { PluginMap } from "./registry";
export { registerPlugins, hasPlugin, getPlugin } from "./registry";
export type { PluginMap, CloudPluginRegister } from "./registry";

// Interfaces
export type {
PluginLogger,
SquadhubProvisioner,
ProvisionParams,
ProvisionResult,
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type { PluginLogger } from "./logger";

export type {
SquadhubProvisioner,
ProvisionParams,
Expand Down
Loading