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
54 changes: 17 additions & 37 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,20 @@ 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: 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. 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.
# The administrator identity `bun run dev` seeds for local development:
# the dev script signs this account up and makes it the owner of the root
# tenant (WORKBENCH_DEFAULT_TENANT below), so a fresh checkout can sign in
# immediately. 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

# 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.

# Slug of the root tenant the hub ensures at boot. Every self-served
# personal bench parents under it, and the env-key auto-plant resolves
# Slug of the root tenant first signup mints. Every self-served
# personal bench parents under it, and `workbench setup`/`seed` 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
Expand Down Expand Up @@ -93,32 +89,16 @@ HUB_STATIC_DIR=../web/dist
# may register (comma-separated). Empty/unset = any domain.
# WORKBENCH_ALLOWED_EMAIL_DOMAINS=acme.example

# Your Anthropic API key — set it for real AI replies. The hub plants it
# as a real, probed credential on the operator bench at hub start (the
# env-key auto-plant). Boot itself does not deploy workflows. Catalog
# rows may appear from that plant when the key is set. A freshly
# self-served personal bench gets the default workflow set through
# onboarding once someone connects a provider.
# ANTHROPIC_API_KEY=

# Every other curated provider's key, read the same way and auto-planted
# the same way at hub start — set any subset of these, or none. See
# packages/onboarding/src/plant-env-credentials.ts (PROVIDER_ENV_VARS)
# for the full, authoritative list.
# OPENAI_API_KEY=
# GEMINI_API_KEY=
# XAI_API_KEY=
# OPENROUTER_API_KEY=
# OPENCODE_ZEN_API_KEY=
# GROQ_API_KEY=
# DEEPSEEK_API_KEY=
# MISTRAL_API_KEY=
# HUGGINGFACE_API_KEY=
#
# Local (or tailscale-tunneled) Ollama origin. Optional: auto-plants a
# probed catalog credential (no key required) on the operator bench at
# hub start, and mounts the `@corbits/memory` plane against the same
# origin when EMBED_BASE_URL is unset (`nomic-embed-text`, ollama style).
# Provider API keys are no longer read from the environment: hub boot
# plants nothing, and setting ANTHROPIC_API_KEY or any other provider's
# env var here has no effect. Connect a provider in the UI (the
# onboarding wizard's connect cards) instead.

# Local (or tailscale-tunneled) Ollama origin. Optional, and still read
# by `bun run dev`'s memory setup: it mounts the `@corbits/memory` plane
# against this origin when EMBED_BASE_URL is unset (`nomic-embed-text`,
# ollama style). Connect the Ollama provider in the UI to give the hub
# itself an inference credential — no key required, just the base URL.
# OLLAMA_BASE_URL=http://localhost:11434

# Set to 1 to make `workbench seed` also deploy the zero-cost
Expand Down
7 changes: 3 additions & 4 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ recorded per-package in each vendored package's own `VENDORED-FROM` file.
`bun run dev` seeds the administrator account and ensures the root
tenant; it does not insert agents, tools, workflows, or skills. An empty
database is a valid hub. `bun run setup` applies migrations against the
running database and is safe to re-run. `ANTHROPIC_API_KEY` is the one
optional variable worth setting before boot — the env-key auto-plant
puts a real credential on the operator bench when it is set; without it,
inference waits until someone connects a provider.
running database and is safe to re-run. Provider API keys are never read
from the environment — hub boot plants no credentials; inference waits
until someone connects a provider.

## Acceptance mechanism: the e2e browser walkthrough

Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,12 @@ inserts no users or tenants. An empty database is a valid hub: boot
does not insert agents, tools, workflows, or skills. Product state
arrives through onboarding and explicit seed callers, not production
boot.
`ANTHROPIC_API_KEY` is the one optional line worth setting before boot —
with it, the env-key auto-plant puts a real credential on the operator
bench so the catalog is launchable; without it, inference waits until
someone connects a provider.
Provider API keys are never read from the environment: hub boot plants no
credentials, so setting `ANTHROPIC_API_KEY` or any other provider's env
var has no effect. Inference waits until someone connects a provider.

Leaving `ANTHROPIC_API_KEY` unset doesn't just apply to the administrator
account: anyone who signs up gets a personal bench with no default routines
That applies to every account, not just the administrator: anyone who
signs up gets a personal bench with no default routines
deployed, and first-run tells them exactly that. Onboarding walks them
through picking a provider — Anthropic, OpenAI, Google, OpenRouter, Hugging
Face, Groq, or another of the curated providers in
Expand Down
122 changes: 11 additions & 111 deletions apps/hub/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,10 @@
// Anything else the hub learns is data in the database, never
// configuration.
//
// ANTHROPIC_API_KEY is the one model-related variable a freshly
// self-served personal bench needs: when set, the hub carries a seed
// model credential (anthropic/claude-sonnet-5) it hands to
// `@workbench/onboarding` so that bench gets the default workflow set
// deployed at first login. Left unset, that deployment step is skipped
// — the bench is still provisioned, only the default workflow
// deployment is skipped, and the skip is logged.
//
// ANTHROPIC_API_KEY and every other curated provider's conventional key
// (`@workbench/onboarding`'s `PROVIDER_ENV_VARS` — OPENAI_API_KEY,
// GEMINI_API_KEY/GOOGLE_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY,
// OPENCODE_ZEN_API_KEY, GROQ_API_KEY, DEEPSEEK_API_KEY, MISTRAL_API_KEY,
// HUGGINGFACE_API_KEY) are also read as an env-key auto-plant (CL-6101):
// once the hub finds its own operator bench (HUB_ADMIN_EMAIL/PASSWORD
// signed in, ORG_SLUG resolved — the same identity `workbench setup` /
// `workbench seed` use), it plants a real, probed credential for every
// key it finds there, making that bench's catalog launchable with no
// `workbench seed` re-run. See `../env-credential-plant.ts`. All of
// these — including HUB_ADMIN_EMAIL/PASSWORD/ORG_SLUG — are optional:
// the plant is skipped, quietly and non-fatally, whenever the admin
// identity cannot be resolved or no provider key is set.
// Provider credentials are never read from the environment: operators
// connect a provider in the UI (or via the same connect API the
// onboarding flow uses), never by setting ANTHROPIC_API_KEY or any
// other provider's env var — hub boot plants nothing.
//
// GOOGLE_CLIENT_ID/SECRET and GITHUB_CLIENT_ID/SECRET are each an
// optional pair: set both to enable that OAuth provider on the sign-in
Expand All @@ -40,11 +23,6 @@
// paste-a-token provider card.

import { type } from "arktype";
import {
envProviderBaseUrlsFrom,
envProviderKeysFrom,
} from "@workbench/onboarding";
import type { SupportedCredentialProvider } from "@corbits/connections/credential-test";

const HTTP_URL = /^https?:\/\/.+$/;

Expand Down Expand Up @@ -83,7 +61,7 @@ const HubEnv = type({
"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"',
'slug of the root tenant the hub ensures at boot; every self-served personal bench parents under it, and setup/seed 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 All @@ -106,50 +84,8 @@ const HubEnv = type({
"ROUTINE_SCHEDULER_POLL_INTERVAL_MS?": type(/^[1-9]\d*$/).describe(
"dev/test-only override for the routine scheduler's poll interval, in milliseconds — unset (default) runs the real 30s production cadence; the e2e harness sets this to a fast interval so a scheduled-routine test doesn't wait out the real cadence",
),
"ANTHROPIC_API_KEY?": type("string > 0").describe(
"your Anthropic API key; optional, enables the default workflow set for freshly self-served benches, and auto-plants a probed catalog credential on the operator bench at hub start",
),
"OPENAI_API_KEY?": type("string > 0").describe(
"your OpenAI API key; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"GEMINI_API_KEY?": type("string > 0").describe(
"your Google Gemini API key; optional, auto-plants a probed catalog credential on the operator bench at hub start — GOOGLE_API_KEY is used when this is unset",
),
"GOOGLE_API_KEY?": type("string > 0").describe(
"your Google Gemini API key, under its other common name; only read when GEMINI_API_KEY is unset",
),
"XAI_API_KEY?": type("string > 0").describe(
"your xAI API key; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"OPENROUTER_API_KEY?": type("string > 0").describe(
"your OpenRouter API key; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"OPENCODE_ZEN_API_KEY?": type("string > 0").describe(
"your Opencode Zen API key; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"GROQ_API_KEY?": type("string > 0").describe(
"your Groq API key; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"DEEPSEEK_API_KEY?": type("string > 0").describe(
"your DeepSeek API key; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"MISTRAL_API_KEY?": type("string > 0").describe(
"your Mistral API key; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"HUGGINGFACE_API_KEY?": type("string > 0").describe(
"your Hugging Face router API token; optional, auto-plants a probed catalog credential on the operator bench at hub start",
),
"OLLAMA_BASE_URL?": type("string > 0").describe(
"the origin your local (or tailscale-tunneled) Ollama instance listens on, e.g. http://localhost:11434; optional, auto-plants a probed catalog credential (no key required) on the operator bench at hub start",
),
"HUB_ADMIN_EMAIL?": type(/^[^@\s]+@[^@\s]+$/).describe(
"the administrator account the env-key auto-plant signs in as to find the operator bench; same identity `workbench setup`/`workbench seed` use — unset falls back to alice@example.com, the same default those commands use",
),
"HUB_ADMIN_PASSWORD?": type("string >= 8").describe(
"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(
'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"',
'alias for WORKBENCH_DEFAULT_TENANT when that is unset — the same root/operator slug setup and seed 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 @@ -273,9 +209,7 @@ function parsePositiveMsEnv(
return n;
}

const DEFAULT_PLANT_ADMIN_EMAIL = "alice@example.com";
const DEFAULT_PLANT_ADMIN_PASSWORD = "password123";
const DEFAULT_PLANT_ORG_SLUG = "workbench";
const DEFAULT_TENANT_SLUG = "workbench";

// One member per implemented `SidecarProvisioner` backend. Adding a new
// backend (e.g. a remote sandbox) is: implement the contract in its own
Expand Down Expand Up @@ -387,31 +321,6 @@ export type HubConfig = {
/** How long an idle chat resident may sit before the hub reaps it via
* a state-preserving undeploy. Defaults to `DEFAULT_CHAT_IDLE_REAP_MS`. */
readonly chatIdleReapMs: number;
/** Every curated provider's key found under its conventional env var
* name (`@workbench/onboarding`'s `PROVIDER_ENV_VARS`). Empty when
* none are set — the env-key auto-plant then does nothing. */
readonly envProviderKeys: Partial<
Record<SupportedCredentialProvider, string>
>;
/** The configured base URL for whichever curated providers carry one
* (`OLLAMA_BASE_URL` today, the only such provider). Empty when unset
* — the env-key auto-plant then probes and seeds ollama, if present in
* `envProviderKeys`, against its own default local origin. */
readonly envProviderBaseUrls: Partial<
Record<SupportedCredentialProvider, string>
>;
/** The identity the env-key auto-plant signs in as to find the
* operator bench — the same identity `workbench setup`/`workbench
* seed` use, defaulted the same way when unset. Always populated
* (never optional): an unset HUB_ADMIN_EMAIL/PASSWORD/ORG_SLUG is a
* valid local-dev shape, not a reason to skip the plant outright —
* the plant itself degrades to a no-op, logged, when this identity
* does not resolve to a real operator bench. */
readonly envCredentialPlantAdmin: {
readonly email: string;
readonly password: string;
readonly orgSlug: string;
};
};

type ParsedHubEnv = typeof HubEnv.infer;
Expand Down Expand Up @@ -646,13 +555,11 @@ export function readHubConfig(
.map((d) => d.trim())
.filter((d) => d.length > 0);

// One deployment fact shared by first-signup genesis, setup/seed, and the
// env-key auto-plant. WORKBENCH_DEFAULT_TENANT wins; ORG_SLUG is the
// alias when that is unset.
// One deployment fact shared by first-signup genesis, setup, and seed.
// 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;
parsed.WORKBENCH_DEFAULT_TENANT ?? parsed.ORG_SLUG ?? DEFAULT_TENANT_SLUG;

const hubConfig: { -readonly [K in keyof HubConfig]: HubConfig[K] } = {
databaseUrl: parsed.DATABASE_URL,
Expand Down Expand Up @@ -683,13 +590,6 @@ export function readHubConfig(
? Number(parsed.SIGNIN_RATE_LIMIT_MAX)
: DEFAULT_SIGNIN_RATE_LIMIT_MAX,
},
envProviderKeys: envProviderKeysFrom(parsed),
envProviderBaseUrls: envProviderBaseUrlsFrom(parsed),
envCredentialPlantAdmin: {
email: parsed.HUB_ADMIN_EMAIL ?? DEFAULT_PLANT_ADMIN_EMAIL,
password: parsed.HUB_ADMIN_PASSWORD ?? DEFAULT_PLANT_ADMIN_PASSWORD,
orgSlug: defaultTenantSlug,
},
chatIdleReapMs: parsePositiveMsEnv(
parsed.WORKBENCH_CHAT_IDLE_REAP_MS,
"WORKBENCH_CHAT_IDLE_REAP_MS",
Expand Down
Loading
Loading