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
42 changes: 18 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,17 @@ shape, and the administrator account (`HUB_ADMIN_EMAIL` / `HUB_ADMIN_PASSWORD`,
defaulting to alice@example.com / password123 when unset)
is seeded so you can sign in immediately.

`bun run dev` only seeds that account — it provisions no bench, catalog, or
workflows. Once the stack is up, run these against it (in another terminal):

```sh
bun run setup
bun run seed
```

`bun run setup` provisions the bench for the administrator account; `bun run
seed` deploys the default workflow set and plants the tenant catalog's model
data, so interactive instances have a model to resolve against. Both read
their configuration from `.env` (see `.env.example`) and are safe to re-run.
`ANTHROPIC_API_KEY` is the one optional line worth setting before you run
`bun run seed` — with it, seeding plants a real credential and the catalog
is actually launchable; without it, everything above still runs, but
inference errors until you set it and re-run `bun run seed`.
`bun run dev` seeds that account and, once the hub is serving, also
provisions and seeds the root tenant itself: publishing the
`corbits-tools` registry, deploying the default workflow set, and
planting the tenant catalog's model data, so interactive instances have
a model to resolve against. This runs automatically on every hub boot
(`apps/hub/src/system-seed.ts`), reads its configuration from `.env`
(see `.env.example`), and is safe to re-run — restarting the hub
re-seeds idempotently. `ANTHROPIC_API_KEY` is the one optional line
worth setting before boot — with it, seeding plants a real credential
and the catalog is actually launchable; without it, everything above
still runs, but inference errors until you set it and restart the hub.

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
Expand All @@ -97,13 +92,13 @@ through picking a provider — Anthropic, OpenAI, Google, OpenRouter, Hugging
Face, Groq, or another of the curated providers in
[`packages/hub-client/src/catalog-seed-data.ts`](packages/hub-client/src/catalog-seed-data.ts)
— and pasting their own key (or, for OpenRouter, completing a PKCE OAuth
connect), which is stored right away — no separate `bun run seed` step, no
connect), which is stored right away — no separate seeding step, no
docs to read. A wrong key isn't caught up front; it surfaces the first time
it's actually dialed for real inference, through the same in-chat "Fix this
connection" flow any credential failure uses. The bench's default agents
deploy in the background — "Your workbench is ready — agents will come
online shortly," no "Connecting…" wait in the browser. Whichever provider they connect gets its own curated
catalog entry planted the same way `bun run seed` plants Anthropic's; see
catalog entry planted the same way boot-time seeding plants Anthropic's; see
[docs/model-seeding.md](docs/model-seeding.md) for how that catalog data is
curated and kept up to date.

Expand All @@ -117,12 +112,11 @@ bun run reset
```

`bun run reset` drops the platform database schema and removes the hub's
and the dev sidecar's on-disk asset directories — everything `bun run setup`
and `bun run seed` created, plus anything provisioned through onboarding.
Nothing is re-seeded: the next `bun run dev` recreates the schema from
scratch and starts you at a fresh sign-up screen. Run `bun run setup && bun
run seed` afterward instead if you want a reprovisioned bench rather than a
blank slate.
and the dev sidecar's on-disk asset directories — everything boot-time
seeding and onboarding created. Nothing is re-seeded until the next `bun
run dev` — that recreates the schema and, once the hub is serving again,
reprovisions and re-seeds the root tenant from scratch, landing you at a
fresh sign-up screen with the administrator's bench ready.

It refuses to run against anything but a local `DATABASE_URL` (localhost,
127.0.0.1, or `::1`) — there is no override, since the schema drop is
Expand Down
9 changes: 4 additions & 5 deletions apps/hub/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,10 @@ const SEED_MODEL_PROVIDER = "anthropic";
const SEED_MODEL = "claude-sonnet-5";
const SEED_MODEL_BASE_URL = "https://api.anthropic.com";

// Matches `workbench setup`/`workbench seed`'s own defaults
// (packages/cli/src/config.ts) exactly, so a zero-.env-edit local
// checkout that seeds its admin account through `bun run dev` also
// resolves the same operator bench for the env-key auto-plant with no
// extra configuration.
// Matches boot-time seeding's own defaults (`system-seed.ts`) exactly,
// so a zero-.env-edit local checkout that seeds its admin account
// through `bun run dev` also resolves the same operator bench for the
// env-key auto-plant with no extra configuration.
const DEFAULT_PLANT_ADMIN_EMAIL = "alice@example.com";
const DEFAULT_PLANT_ADMIN_PASSWORD = "password123";
const DEFAULT_PLANT_ORG_SLUG = "workbench";
Expand Down
13 changes: 13 additions & 0 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ import {
import { generateId } from "@intx/hub-common";

import { ensureDefaultTenant } from "./default-tenant";
import { runSystemSeed } from "./system-seed";
import {
createInMemoryMailboxEventBus,
createMailboxDb,
Expand Down Expand Up @@ -3658,6 +3659,18 @@ if (import.meta.main) {
});
const log = getLogger(["hub"]);
log.info`Hub serving on port ${port}`;
// CL-7382: replaces `workbench seed`. Runs against the hub's own real
// origin now that it is actually listening — `runSystemSeed`'s
// workflow push needs a reachable origin for `git push`, not just an
// in-process fetch entry point. Never awaited: a slow or still-
// sidecar-less seed must not delay "Hub serving" or hold up shutdown
// wiring below it.
void runSystemSeed({
baseUrl: config.baseUrl,
orgSlug: config.defaultTenantSlug,
admin: config.envCredentialPlantAdmin,
...(config.seedModel !== undefined ? { seedModel: config.seedModel } : {}),
});
const SHUTDOWN_DRAIN_MS = 10_000;
// In-flight Hono handlers (a request mid-Postgres-transaction, a git
// write, anything that has not returned a Response yet) must finish
Expand Down
162 changes: 162 additions & 0 deletions apps/hub/src/system-seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// CL-7382: seeds the root tenant's default workflow set and catalog at
// hub boot — the boot-time replacement for `workbench seed`. Runs
// against the hub's real, already-listening origin (`config.baseUrl`),
// not an in-process `fetch` entry point the way `env-credential-plant.ts`
// runs: `seedTenant`'s workflow push shells out a real `git push` over
// HTTP, so it needs a reachable origin to exist, not just a composed
// app object.
//
// The deploy step needs a connected sidecar; until one dials in, every
// deploy answers 502 and `seedTenant` throws (see
// `scripts/e2e/local-rip.test.ts`'s own `deploySeededWorkflows` retry
// loop, which this mirrors). Boot itself must never block on that or
// fail over it — `bun run dev` starts the hub and sidecar together, but
// they race, and a production boot may reasonably outlive its sidecar's
// own startup. So this polls with a bounded deadline and gives up
// quietly, logged once: every step here is ensure-then-create and every
// workflow push is content-addressed and skips an unchanged tree, so
// the very next boot picks up exactly where this one left off.

import { reportError } from "@corbits/error-sink";
import { getLogger } from "@intx/log";
import {
createHubAPI,
signIn,
seedTenant,
seedCatalog,
ensureDefaultRoutines,
createGitWorkflowPusher,
publishCorbitsToolsRegistry,
DEFAULT_WORKFLOWS,
PLACEHOLDER_CATALOG_API_KEY,
type ApiCall,
type ModelSource,
} from "@workbench/hub-client";
import { findPersonalTenant } from "@workbench/onboarding";

const log = getLogger(["hub", "system-seed"]);

const DEFAULT_DEADLINE_MS = 60_000;
const DEFAULT_POLL_INTERVAL_MS = 2_000;

// Matches `workbench seed`'s own default model source (readSeedConfig in
// the now-deleted `packages/cli/src/config.ts`): real anthropic/
// claude-sonnet-5 when a hub-owned key is configured (`config.seedModel`),
// a placeholder key otherwise so the default workflow set still deploys
// and the catalog is still browsable, just not launchable.
const SEED_MODEL_PROVIDER = "anthropic";
const SEED_MODEL = "claude-sonnet-5";
const SEED_MODEL_BASE_URL = "https://api.anthropic.com";

function resolvedModel(seedModel: ModelSource | undefined): ModelSource {
return (
seedModel ?? {
provider: SEED_MODEL_PROVIDER,
model: SEED_MODEL,
baseURL: SEED_MODEL_BASE_URL,
apiKey: PLACEHOLDER_CATALOG_API_KEY,
}
);
}

export type SystemSeedDeps = {
baseUrl: string;
orgSlug: string;
admin: { email: string; password: string };
seedModel?: ModelSource;
deadlineMs?: number;
pollIntervalMs?: number;
};

/**
* Seeds the root tenant once: the default workflow set, then the tenant
* catalog (a real credential when a hub-owned seed model is configured, a
* placeholder one otherwise), then the default routine presets. Never
* throws — a failure that persists past the deadline is logged and left
* for the next boot to retry, the same "safe to re-run" property
* `workbench seed` always had.
*/
export async function runSystemSeed(deps: SystemSeedDeps): Promise<void> {
const api: ApiCall = createHubAPI(deps.baseUrl);
const model = resolvedModel(deps.seedModel);
const deadline = Date.now() + (deps.deadlineMs ?? DEFAULT_DEADLINE_MS);
const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
const pushWorkflow = createGitWorkflowPusher();

let lastLoggedReason: string | undefined;
for (;;) {
try {
const session = await signIn(api, deps.admin);
const tenant = await findPersonalTenant(
api,
session.cookies,
deps.orgSlug,
);
if (tenant === undefined) {
throw new Error(
`root bench "${deps.orgSlug}" is not visible to ${deps.admin.email} yet`,
);
}

// `workbench setup` used to publish `corbits-tools` onto the root
// explicitly, before `workbench seed` ran. Every descendant tenant
// inherits the registry from its parent, so this is the one place
// it needs publishing — packing is content-addressed and skips an
// already-present filename, so a re-publish on every boot is cheap.
await publishCorbitsToolsRegistry({
api,
cookies: session.cookies,
hubUrl: deps.baseUrl,
tenantId: tenant.tenantId,
log: (line) => log.info`${line}`,
});

await seedTenant({
api,
cookies: session.cookies,
hubUrl: deps.baseUrl,
tenant: {
tenantId: tenant.tenantId,
principalId: tenant.principalId,
domain: tenant.tenantDomain,
},
model,
pushWorkflow,
log: (line) => log.info`${line}`,
workflows: DEFAULT_WORKFLOWS,
});

await seedCatalog({
api,
cookies: session.cookies,
tenantId: tenant.tenantId,
log: (line) => log.info`${line}`,
...(deps.seedModel !== undefined
? { apiKey: deps.seedModel.apiKey }
: { placeholderCredential: true }),
});

await ensureDefaultRoutines(
api,
session.cookies,
tenant.tenantId,
(line) => log.info`${line}`,
);

log.info`root tenant seed complete`;
return;
} catch (cause) {
const reason = cause instanceof Error ? cause.message : String(cause);
if (Date.now() >= deadline) {
reportError(cause, { operation: "system-seed.seedRootTenant" });
log.error`root tenant seed did not complete before its deadline (last error: ${reason}); the next boot will retry`;
return;
}
if (reason !== lastLoggedReason) {
log.info`root tenant seed not ready yet (${reason}); retrying`;
lastLoggedReason = reason;
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
}
}
32 changes: 0 additions & 32 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/TENANCY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ walks the chain on every read.
### Root tenant slug (one deployment fact)

The hub ensures a root tenant at boot by slug. That same slug is the
operator bench `workbench setup` / `workbench seed` and the env-key
auto-plant resolve:
operator bench boot-time seeding (`apps/hub/src/system-seed.ts`) and the
env-key auto-plant resolve:

1. `WORKBENCH_DEFAULT_TENANT` if set
2. else `ORG_SLUG` (alias)
Expand Down
Loading
Loading