Skip to content
Merged
36 changes: 17 additions & 19 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,32 @@ HUB_ALLOW_GIT_INSIDE_WORK_TREE=1
# with an index.html works — ../hub/public is a minimal placeholder page.
HUB_STATIC_DIR=../web/dist

# The administrator account: `bun run dev` seeds it once the hub answers
# so a fresh checkout can sign in immediately, and `workbench setup` /
# `workbench seed` authenticate as it. Sign in with these credentials
# right away. In `bun run dev`, leave either value empty to skip seeding
# (hosted deployments do not run `bun run dev`). The hub itself also
# authenticates as this account (and resolves ORG_SLUG below) to find
# the operator bench for the env-key auto-plant — see ANTHROPIC_API_KEY
# further down.
# The administrator account: the hub seeds it at boot and makes it the
# owner of the root tenant (WORKBENCH_DEFAULT_TENANT below), so a fresh
# checkout can sign in immediately and `workbench setup` / `workbench
# seed` authenticate as it. Sign in with these credentials right away.
# Unset values fall back to the defaults shown. The hub itself also
# authenticates as this account (and resolves the same root slug) to
# find the operator bench for the env-key auto-plant — see
# ANTHROPIC_API_KEY further down.
# Administrator identity. Unset values fall back to the defaults shown —
# fine for local development, set both for real deployments.
# HUB_ADMIN_EMAIL=alice@example.com
# HUB_ADMIN_PASSWORD=password123

# The operator bench's slug — `workbench setup` creates it, `workbench
# seed` and the hub's own env-key auto-plant resolve it. Unset falls
# back to "workbench", the same default those commands use.
# ORG_SLUG=workbench

# Everything below is optional. Leave a variable unset to leave the
# feature it configures off; the hub never treats a partially-set group
# as configured — it fails loudly at boot instead.

# The tenant every self-served personal bench is parented under.
# `workbench setup` writes this to the org tenant it created. Leave it
# blank only for isolated tests that need an unparented personal bench
# — that is not the default self-serve story. Restart the hub after
# setup so it reads the new value.
# OPERATOR_TENANT_ID=
# Slug of the root tenant the hub ensures at boot. Every self-served
# personal bench parents under it, and `workbench setup` / `workbench
# seed` / the env-key auto-plant resolve the same slug. Unset falls back
# to "workbench". ORG_SLUG is an alias when this is unset — set only one.
# Upgrading a deploy whose existing root was not "workbench": set this
# to that slug (do not leave OPERATOR_TENANT_ID; the hub refuses to boot
# while that stale key is set).
# WORKBENCH_DEFAULT_TENANT=workbench
# ORG_SLUG=workbench

# Per-IP rate limit on email sign-up. Defaults to 5 sign-ups per 60
# seconds when unset.
Expand Down
31 changes: 24 additions & 7 deletions apps/hub/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ const HubEnv = type({
HUB_STATIC_DIR: type("string > 0").describe(
"a directory of built user-interface files the hub serves, e.g. apps/hub/public",
),
"OPERATOR_TENANT_ID?": type("string > 0").describe(
"the tenant id every self-served personal bench is parented under; workbench setup writes this for the org tenant it creates. Leave unset only for isolated tests that need an unparented personal bench",
"WORKBENCH_DEFAULT_TENANT?": type(
/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/,
).describe(
'slug of the root tenant the hub ensures at boot; every self-served personal bench parents under it, and setup/seed/plant resolve the same slug — ORG_SLUG is an alias when this is unset; default "workbench"',
),
"SIGNUP_RATE_LIMIT_WINDOW_SECONDS?": type(/^[1-9]\d*$/).describe(
"the per-IP sign-up rate-limit window, in seconds, e.g. 60",
Expand Down Expand Up @@ -147,7 +149,7 @@ const HubEnv = type({
"the administrator password the env-key auto-plant signs in with; unset falls back to password123, the same default `workbench setup`/`workbench seed` use",
),
"ORG_SLUG?": type(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/).describe(
'the operator bench slug the env-key auto-plant resolves; same variable `workbench setup`/`workbench seed` read — unset falls back to "workbench"',
'alias for WORKBENCH_DEFAULT_TENANT when that is unset — same root/operator slug the hub, setup, seed, and the env-key auto-plant resolve; default "workbench"',
),
"GOOGLE_CLIENT_ID?": type("string > 0").describe(
"Google OAuth client id; set together with GOOGLE_CLIENT_SECRET to enable Google sign-in",
Expand Down Expand Up @@ -327,7 +329,7 @@ export type HubConfig = {
readonly sessionSecret: string;
readonly hubDataDir: string;
readonly hubStaticDir: string;
readonly operatorTenantId?: string;
readonly defaultTenantSlug: string;
readonly signupRateLimit: {
readonly windowSeconds: number;
readonly max: number;
Expand Down Expand Up @@ -606,6 +608,14 @@ function seedModelFrom(parsed: ParsedHubEnv): ModelSource | undefined {
export function readHubConfig(
env: Record<string, string | undefined>,
): HubConfig {
if (env.OPERATOR_TENANT_ID !== undefined) {
throw new Error(
"OPERATOR_TENANT_ID is no longer read: the hub ensures the root tenant by slug at boot. " +
"Set WORKBENCH_DEFAULT_TENANT to your existing root tenant's slug " +
'(or remove OPERATOR_TENANT_ID to keep the default slug "workbench"), then restart.',
);
}

const parsed = HubEnv(env);
if (parsed instanceof type.errors) {
throw new Error(
Expand All @@ -628,12 +638,21 @@ export function readHubConfig(
.map((d) => d.trim())
.filter((d) => d.length > 0);

// One deployment fact shared by boot parenting, setup/seed, and the
// env-key auto-plant. WORKBENCH_DEFAULT_TENANT wins; ORG_SLUG is the
// alias when that is unset.
const defaultTenantSlug =
parsed.WORKBENCH_DEFAULT_TENANT ??
parsed.ORG_SLUG ??
DEFAULT_PLANT_ORG_SLUG;

const hubConfig: { -readonly [K in keyof HubConfig]: HubConfig[K] } = {
databaseUrl: parsed.DATABASE_URL,
baseUrl: parsed.BASE_URL,
sessionSecret: parsed.SESSION_SECRET,
hubDataDir: parsed.HUB_DATA_DIR,
hubStaticDir: parsed.HUB_STATIC_DIR,
defaultTenantSlug,
socialProviders,
signupMode: parsed.WORKBENCH_SIGNUP ?? "closed",
allowedEmailDomains,
Expand Down Expand Up @@ -661,16 +680,14 @@ export function readHubConfig(
envCredentialPlantAdmin: {
email: parsed.HUB_ADMIN_EMAIL ?? DEFAULT_PLANT_ADMIN_EMAIL,
password: parsed.HUB_ADMIN_PASSWORD ?? DEFAULT_PLANT_ADMIN_PASSWORD,
orgSlug: parsed.ORG_SLUG ?? DEFAULT_PLANT_ORG_SLUG,
orgSlug: defaultTenantSlug,
},
chatIdleReapMs: parsePositiveMsEnv(
parsed.WORKBENCH_CHAT_IDLE_REAP_MS,
"WORKBENCH_CHAT_IDLE_REAP_MS",
DEFAULT_CHAT_IDLE_REAP_MS,
),
};
if (parsed.OPERATOR_TENANT_ID !== undefined)
hubConfig.operatorTenantId = parsed.OPERATOR_TENANT_ID;
if (parsed.HUB_ALLOW_GIT_INSIDE_WORK_TREE !== undefined)
hubConfig.allowGitInsideWorkTree = true;
if (parsed.PORT !== undefined) hubConfig.listenPort = Number(parsed.PORT);
Expand Down
Loading
Loading