diff --git a/apps/hub/package.json b/apps/hub/package.json index 6f1575e88..f12deaf1d 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -14,6 +14,7 @@ "dependencies": { "@corbits/agent-directory": "workspace:*", "@corbits/approvals": "workspace:*", + "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#81049ed24a64e927498c7238bda6ffa66b63d2ab", "@corbits/chat": "workspace:*", "@corbits/commands": "workspace:*", "@corbits/folded-runs": "workspace:*", diff --git a/apps/hub/src/artifacts-mount.ts b/apps/hub/src/artifacts-mount.ts new file mode 100644 index 000000000..40fb761c5 --- /dev/null +++ b/apps/hub/src/artifacts-mount.ts @@ -0,0 +1,61 @@ +/** + * Hub-side artifacts engine mount — the host's own analog of + * `@corbits/dock`'s `mountArtifacts` (see Scout's + * `packages/agent-dock/src/artifacts-store.ts`). `@corbits/artifacts` + * (git pin) persists artifacts + immutable version history in Postgres; + * its `artifact`/`artifact_version` tables carry hard FKs into the + * host's own `public.tenant` / `public.principal` tables, so the engine + * MUST point at the same Postgres cluster as this hub's control plane. + * + * Degrades cleanly when unconfigured: `ARTIFACTS_DATABASE_URL` unset + * (and no explicit `databaseUrl` passed) means "no artifacts + * persistence", logged once at boot, never thrown — same contract as + * the dock mount. + * + * This module lands the mount + factory only. Tenant-scoped HTTP + * list/search/read routes are intentionally not registered here yet — + * Library still reads the asset-shim surface until those routes ship. + */ +import { getLogger } from "@intx/log"; +import { + createArtifactDb, + runArtifactMigrations, + type ArtifactDb, +} from "@corbits/artifacts"; + +const log = getLogger(["hub", "artifacts-mount"]); + +export type MountArtifactsOptions = { + /** Defaults to `process.env.ARTIFACTS_DATABASE_URL`. */ + databaseUrl?: string; +}; + +/** + * Handle returned by a successful mount. The `db` is the engine's own + * drizzle handle (the same shape dock's `mountArtifacts` exposes) so a + * later routes module can build the persist/find/search/read surface on + * top of it without re-deriving the connection. + */ +export type ArtifactsMountHandle = { + db: ArtifactDb; +}; + +export async function mountArtifacts( + options: MountArtifactsOptions = {}, +): Promise { + const databaseUrl = + options.databaseUrl ?? process.env["ARTIFACTS_DATABASE_URL"]; + if (!databaseUrl) { + log.info( + "ARTIFACTS_DATABASE_URL not set — artifacts will not be persisted", + ); + return undefined; + } + + const { db } = createArtifactDb(databaseUrl); + await runArtifactMigrations(db); + log.info( + "Artifacts engine mounted — artifacts persist as versioned rows by kind", + ); + return { db }; +} diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index 5478a8ff1..301a6c781 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -57,6 +57,7 @@ import { createNeedsYouRoutes } from "@corbits/approvals"; import { createEchoRoutes } from "@workbench/echo"; import { createGitWorkflowPusher } from "@workbench/hub-client"; import { createOnboardingRoutes } from "@workbench/onboarding"; +import { mountArtifacts } from "./artifacts-mount"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { type Context, type Next } from "hono"; @@ -480,6 +481,32 @@ export async function createHub(config: HubConfig) { app.route("/api/onboarding", createOnboardingRoutes(onboardingDeps)); + // Artifacts engine: mounts `@corbits/artifacts` against the same + // Postgres cluster as this hub's control plane (its + // `artifact`/`artifact_version` tables FK into `public.tenant` / + // `public.principal`). Degrades to a no-op when + // `ARTIFACTS_DATABASE_URL` is unset. The handle is available for + // tenant-scoped list/search/read routes; Library still uses the + // asset-shim surface until those land. + // + // The mount runs migrations against the configured DB; if the URL is + // present but points at an unreachable/invalid cluster the migration + // would otherwise throw and take the whole hub down at boot. We catch + // that here so the hub comes up in a degraded (no-artifacts) mode and + // surfaces the failure as a warning rather than a crash. + let artifactsHandle: Awaited>; + try { + artifactsHandle = await mountArtifacts(); + } catch (error) { + log.warn( + `Artifacts mount failed — continuing without artifacts persistence: ${error}`, + ); + artifactsHandle = undefined; + } + log.info( + `Artifacts handle ${artifactsHandle !== undefined ? "available" : "unavailable (degraded mode)"}`, + ); + // Tells the signed-out screen which OAuth buttons to draw, without // exposing the credentials themselves — just which providers a full // pair was configured for. No session or tenant is required to ask, diff --git a/apps/hub/tsconfig.json b/apps/hub/tsconfig.json index e956ddd88..43a971efb 100644 --- a/apps/hub/tsconfig.json +++ b/apps/hub/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "types": ["bun"] + "types": ["bun"], + "customConditions": ["bun"] }, "include": ["src", "test"] } diff --git a/bun.lock b/bun.lock index affe86385..ab693cb6d 100644 --- a/bun.lock +++ b/bun.lock @@ -23,6 +23,7 @@ "dependencies": { "@corbits/agent-directory": "workspace:*", "@corbits/approvals": "workspace:*", + "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#81049ed24a64e927498c7238bda6ffa66b63d2ab", "@corbits/chat": "workspace:*", "@corbits/commands": "workspace:*", "@corbits/folded-runs": "workspace:*", @@ -841,6 +842,8 @@ "@corbits/artifact-ui": ["@corbits/artifact-ui@workspace:packages/artifact-ui"], + "@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], + "@corbits/assistant-workflow": ["@corbits/assistant-workflow@workspace:workflows/assistant"], "@corbits/bench-ui": ["@corbits/bench-ui@workspace:packages/bench-ui"],