diff --git a/control-plane/.env.base b/control-plane/.env.base new file mode 100644 index 00000000..515e92cc --- /dev/null +++ b/control-plane/.env.base @@ -0,0 +1,21 @@ +NODE_ENV="development" + +APP_ORIGIN="http://localhost:3001" + +DATABASE_SSL_DISABLED="true" +DATABASE_URL="postgresql://inferable:inferable@localhost:5432/inferable" + +REDIS_URL="redis://localhost:6379" + +SQS_BASE_QUEUE_URL='http://localhost:9324/000000000000' +SQS_RUN_PROCESS_QUEUE_URL='http://localhost:9324/000000000000/run-process' +SQS_RUN_GENERATE_NAME_QUEUE_URL='http://localhost:9324/000000000000/run-generate-name' +SQS_CUSTOMER_TELEMETRY_QUEUE_URL='http://localhost:9324/000000000000/customer-telemetry' +SQS_EXTERNAL_TOOL_CALL_QUEUE_URL='http://localhost:9324/000000000000/external-tool-call' + +# JWKS_URL= +# MANAGEMENT_API_SECRET= + +# ANTHROPIC_API_KEY= +# COHERE_API_KEY= +# BEDROCK_AVAILABLE= diff --git a/control-plane/Dockerfile b/control-plane/Dockerfile index a64c3211..0275bd36 100644 --- a/control-plane/Dockerfile +++ b/control-plane/Dockerfile @@ -34,6 +34,7 @@ ENV SHORT_VERSION=$SHORT_VERSION COPY --from=build /build/ /app ENV NODE_ENV=production +ENV ENVIRONMENT=prod EXPOSE 4000 CMD [ "npm", "run", "start" ] diff --git a/control-plane/README.md b/control-plane/README.md index f3ff3b05..800fe8b2 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -21,6 +21,64 @@ Inferable's control-plane is open-source and self-hostable. [Self hosting guide](https://docs.inferable.ai/pages/self-hosting) +### Local Development + +To run the control plane locally for development: + +1. Start the local resources required for development: +```bash +docker compose -f docker-compose.dev.yml up +``` + +This will start: +- PostgreSQL database with pgvector +- Redis for caching +- ElasticMQ for a local SQS-compatible queue implementation + +2. Populate environment variables: + +Development environment varaiables are managed in the `.env`. + +`.env.base` contains a base set of required environment variables. Copy `.env.base` to `.env`. + +```base +cp .env.base .env +``` + +You will need to populate the following environment variables in `.env`: + +- Model provider API keys (`ANTHROPIC_API_KEY` and `COHERE_API_KEY`) OR `BEDROCK_AVAILABLE` + - If you specify `BEDROCK_AVAILABLE` ensure your environment has access to AWS Bedrock. (See [routing.ts](https://github.com/inferablehq/inferable/blob/main/control-plane/src/modules/models/routing.ts) for model requirements) +- `JWKS_URL` OR `MANAGEMENT_API_SECRET` (For headless mode) + +4. Run DB migrations: + +Inferable uses [drizzle](https://github.com/drizzle-team/drizzle-orm) to manage database migrations. To run migrations: + +```bash +npm run migrate +``` + +3. Start the control plane: +```bash +npm run dev +``` + +The API will be available at `http://localhost:4000`. + +4. Connect via the CLI (Optional): + +```bash +npm install -g @inferable/cli +export INFERABLE_API_ENDPOINT=http://localhost:4000 + +# If running in headless mode, you will be prompted for the management API secret +inf auth login + +# Create a new cluster +inf clusters create +``` + ## Documentation - [Inferable documentation](https://docs.inferable.ai/) contains all the information you need to get started with Inferable. diff --git a/control-plane/docker-compose.dev.yml b/control-plane/docker-compose.dev.yml index c94fc75f..0c07cc1e 100644 --- a/control-plane/docker-compose.dev.yml +++ b/control-plane/docker-compose.dev.yml @@ -5,9 +5,9 @@ services: image: pgvector/pgvector:pg16 container_name: postgres environment: - POSTGRES_USER: myuser - POSTGRES_PASSWORD: mypassword - POSTGRES_DB: mydb + POSTGRES_USER: inferable + POSTGRES_PASSWORD: inferable + POSTGRES_DB: inferable ports: - "5432:5432" volumes: @@ -43,4 +43,6 @@ configs: queues { run-process {} run-generate-name {} + customer-telemetry {} + external-tool-call {} } diff --git a/control-plane/src/index.ts b/control-plane/src/index.ts index 647b0b25..6d78abf7 100644 --- a/control-plane/src/index.ts +++ b/control-plane/src/index.ts @@ -111,16 +111,16 @@ app.addHook("onRequest", (request, _reply, done) => { const startTime = Date.now(); (async function start() { - logger.info("Starting server"); + logger.info("Starting server", { + environment: env.ENVIRONMENT, + ee: env.EE_DEPLOYMENT, + headless: !!env.MANAGEMENT_API_SECRET + }); if (env.ENVIRONMENT === "prod") { - await runMigrations().then(() => { - logger.info("Database migrated", { latency: Date.now() - startTime }); - }); - } + await runMigrations() - if (!env.EE_DEPLOYMENT) { - logger.info("Running in hobby mode"); + logger.info("Database migrated", { latency: Date.now() - startTime }); } await Promise.all([ @@ -130,14 +130,14 @@ const startTime = Date.now(); workflows.start(), knowledge.start(), models.start(), + redis.start(), ...(env.EE_DEPLOYMENT ? [ flagsmith?.getEnvironmentFlags(), - analytics.start(), customerTelemetry.start(), + analytics.start(), toolhouse.start(), externalCalls.start(), - redis.start(), ] : []), ]) diff --git a/control-plane/src/modules/auth/auth.ts b/control-plane/src/modules/auth/auth.ts index 7f4a909a..77afb1ee 100644 --- a/control-plane/src/modules/auth/auth.ts +++ b/control-plane/src/modules/auth/auth.ts @@ -113,17 +113,17 @@ export const plugin = fastifyPlugin(async (fastify: FastifyInstance) => { export const extractAuthState = async ( token: string, ): Promise => { - // Master Secret support (Hobby deployments only) - if (token && token === env.MASTER_API_SECRET) { + // Management Secret support (Hobby deployments only) + if (token && token === env.MANAGEMENT_API_SECRET) { // This is also validated on startup if (env.EE_DEPLOYMENT) { - throw new Error("Can not use master secret in EE deployment"); + throw new Error("Can not use management secret in EE deployment"); } return { type: "api", - entityId: "MASTER_API_SECRET", - organizationId: "MASTER", + entityId: "MANAGEMENT_API_SECRET", + organizationId: "ROOT", canAccess: async function () { return this; }, @@ -134,14 +134,14 @@ export const extractAuthState = async ( return this; }, isMachine: function () { - throw new AuthenticationError("Master API secret auth is not machine"); + throw new AuthenticationError("Management API secret auth is not machine"); }, isClerk: function () { - throw new AuthenticationError("Master API secret auth is not clerk"); + throw new AuthenticationError("Management API secret auth is not clerk"); }, isCustomerProvided: function () { throw new AuthenticationError( - "Master API secret auth is not customer provided", + "Management API secret auth is not customer provided", ); }, isAdmin: function () { diff --git a/control-plane/src/utilities/env.ts b/control-plane/src/utilities/env.ts index 299479bb..25141a06 100644 --- a/control-plane/src/utilities/env.ts +++ b/control-plane/src/utilities/env.ts @@ -27,7 +27,7 @@ const envSchema = z LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"), ENABLE_FASTIFY_LOGGER: truthy.default(false), - MASTER_API_SECRET: z.string().optional(), + MANAGEMENT_API_SECRET: z.string().optional(), DATABASE_URL: z.string().url(), DATABASE_SSL_DISABLED: truthy.default(false), @@ -36,25 +36,24 @@ const envSchema = z JOB_LONG_POLLING_TIMEOUT: z.number().default(15), + REDIS_URL: z.string().url(), + ANTHROPIC_API_KEY: z.string().optional(), COHERE_API_KEY: z.string().optional(), SQS_RUN_PROCESS_QUEUE_URL: z.string(), SQS_RUN_GENERATE_NAME_QUEUE_URL: z.string(), + SQS_LEARNING_INGEST_QUEUE_URL: z.string().optional(), + SQS_CUSTOMER_TELEMETRY_QUEUE_URL: z.string(), + SQS_EXTERNAL_TOOL_CALL_QUEUE_URL: z.string(), SQS_BASE_QUEUE_URL: z.string().optional(), // Required in EE (Disabled by default) EE_DEPLOYMENT: truthy.default(false), - SQS_LEARNING_INGEST_QUEUE_URL: z.string().optional(), - SQS_CUSTOMER_TELEMETRY_QUEUE_URL: z.string().optional(), - SQS_EXTERNAL_TOOL_CALL_QUEUE_URL: z.string().optional(), - APP_ORIGIN: z.string().url().optional(), - REDIS_URL: z.string().url().optional(), - JWKS_URL: z.string().url().optional(), JWT_IGNORE_EXPIRATION: truthy.default(false), @@ -71,15 +70,29 @@ const envSchema = z ANALYTICS_BUCKET_NAME: z.string().optional(), }) .superRefine((value, ctx) => { + if (!value.MANAGEMENT_API_SECRET && !value.JWKS_URL) { + return ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "MANAGEMENT_API_SECRET or JWKS_URL is required", + path: ["MANAGEMENT_API_SECRET", "JWKS_URL"], + }); + } + + if (value.MANAGEMENT_API_SECRET) { + if (value.JWKS_URL) { + return ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "MANAGEMENT_API_SECRET can not be set with JWKS_URL (Headless mode only)", + path: ["MANAGEMENT_API_SECRET"], + }); + } + } + if (!value.EE_DEPLOYMENT) { return; } const EE_REQUIRED = [ - "SQS_LEARNING_INGEST_QUEUE_URL", - "SQS_CUSTOMER_TELEMETRY_QUEUE_URL", - "SQS_EXTERNAL_TOOL_CALL_QUEUE_URL", "APP_ORIGIN", - "REDIS_URL", "JWKS_URL", "HYPERDX_API_KEY", "ROLLBAR_ACCESS_TOKEN", @@ -89,14 +102,6 @@ const envSchema = z "ANALYTICS_BUCKET_NAME", ]; - if (value.MASTER_API_SECRET) { - return ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "MASTER_API_SECRET can not be set for EE Deployment", - path: ["MASTER_API_SECRET"], - }); - } - for (const key of EE_REQUIRED) { //eslint-disable-next-line @typescript-eslint/no-explicit-any if (!(value as any)[key]) {