From 1e31fa09919fa375c5e0ed0064b319fd096c6b9e Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 9 Aug 2026 15:04:30 -0700 Subject: [PATCH 1/7] CL-5291: Mount @corbits/artifacts on the hub when configured Pin the Scout-proven artifacts package and boot-mount it against ARTIFACTS_DATABASE_URL so versioned artifact rows can share the hub Postgres tenancy. Unset URL degrades to a no-op; Library HTTP routes are not part of this change. --- apps/hub/package.json | 1 + apps/hub/src/artifacts-mount.ts | 58 +++++++++++++++++++++++++++++++++ apps/hub/src/index.ts | 10 ++++++ apps/hub/tsconfig.json | 3 +- bun.lock | 3 ++ 5 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 apps/hub/src/artifacts-mount.ts diff --git a/apps/hub/package.json b/apps/hub/package.json index 7dc0f7b33..6a05baf64 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..4ddcdff74 --- /dev/null +++ b/apps/hub/src/artifacts-mount.ts @@ -0,0 +1,58 @@ +/** + * 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 e84cd088b..f2e632465 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -58,6 +58,7 @@ import { createEchoRoutes } from "@workbench/echo"; import { createGitWorkflowPusher } from "@workbench/hub-client"; import { createOnboardingRoutes } from "@workbench/onboarding"; import { mountMemory } from "./memory-mount"; +import { mountArtifacts } from "./artifacts-mount"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { type Context, type Next } from "hono"; @@ -490,6 +491,15 @@ 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. + await mountArtifacts(); + // 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 ca52f6576..165fc721f 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:*", @@ -842,6 +843,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"], From cac1f3f32d22224c91ab36ec3f877ccf52bd4641 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 9 Aug 2026 16:25:38 -0700 Subject: [PATCH 2/7] Format with Prettier for CI --- apps/hub/src/artifacts-mount.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/hub/src/artifacts-mount.ts b/apps/hub/src/artifacts-mount.ts index 4ddcdff74..40fb761c5 100644 --- a/apps/hub/src/artifacts-mount.ts +++ b/apps/hub/src/artifacts-mount.ts @@ -43,7 +43,8 @@ export type ArtifactsMountHandle = { export async function mountArtifacts( options: MountArtifactsOptions = {}, ): Promise { - const databaseUrl = options.databaseUrl ?? process.env["ARTIFACTS_DATABASE_URL"]; + const databaseUrl = + options.databaseUrl ?? process.env["ARTIFACTS_DATABASE_URL"]; if (!databaseUrl) { log.info( "ARTIFACTS_DATABASE_URL not set — artifacts will not be persisted", @@ -53,6 +54,8 @@ export async function mountArtifacts( const { db } = createArtifactDb(databaseUrl); await runArtifactMigrations(db); - log.info("Artifacts engine mounted — artifacts persist as versioned rows by kind"); + log.info( + "Artifacts engine mounted — artifacts persist as versioned rows by kind", + ); return { db }; } From 1a8671476768b350223c3f58202f27ec266c7bbe Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 9 Aug 2026 16:52:53 -0700 Subject: [PATCH 3/7] Guard artifacts mount against boot failure; capture handle --- apps/hub/src/index.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index f2e632465..de91e6e56 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -498,7 +498,24 @@ export async function createHub(config: HubConfig) { // `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. - await mountArtifacts(); + // + // 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 From 84422b91e96dab9a7ea01cbf5009728735868308 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 9 Aug 2026 16:59:12 -0700 Subject: [PATCH 4/7] CL-5858: Tenant-scoped Library L2 list/get artifact routes Wire list + get HTTP over the mounted @corbits/artifacts engine under /api/tenants/:tenantId/artifacts, with grant checks on the asset family. --- apps/hub/src/artifact-routes.test.ts | 137 +++++++++++++++++++++++++++ apps/hub/src/artifact-routes.ts | 115 ++++++++++++++++++++++ apps/hub/src/artifacts-mount.ts | 4 +- apps/hub/src/index.ts | 23 +++-- 4 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 apps/hub/src/artifact-routes.test.ts create mode 100644 apps/hub/src/artifact-routes.ts diff --git a/apps/hub/src/artifact-routes.test.ts b/apps/hub/src/artifact-routes.test.ts new file mode 100644 index 000000000..544d50017 --- /dev/null +++ b/apps/hub/src/artifact-routes.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, test } from "bun:test"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import { Hono } from "hono"; + +import { + createArtifactRoutes, + type ArtifactRoutesStore, + type ArtifactListPage, +} from "./artifact-routes"; +import type { SerializedArtifact } from "@corbits/artifacts"; + +function listItem( + id: string, + tenantId: string, +): ArtifactListPage["data"][number] { + return { + id, + kind: "document", + title: `Title ${id}`, + source: { origin: "manual" }, + version: 1, + ownerPrincipalId: null, + ownerName: null, + archivedAt: null, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-02T00:00:00.000Z", + }; +} + +function detail(id: string, tenantId: string): SerializedArtifact { + return { + ...listItem(id, tenantId), + content: `body of ${id}`, + }; +} + +function memoryStore(seed: { + listByTenant: Record; + details: Record; +}): ArtifactRoutesStore { + return { + async list(tenantId, _opts) { + const data = seed.listByTenant[tenantId] ?? []; + return { data, nextCursor: null }; + }, + async get(tenantId, artifactId) { + const hit = seed.details[artifactId]; + if (hit === undefined || hit.tenantId !== tenantId) return null; + return hit.row; + }, + }; +} + +/** Pass-through grant middleware for route unit tests (authz is hub-owned). */ +const allowAll: RequireGrant = () => async (_c, next) => { + await next(); +}; + +function appWith( + store: ArtifactRoutesStore, + tenantId: string, +): Hono { + const routes = createArtifactRoutes({ store, requireGrant: allowAll }); + const outer = new Hono(); + outer.use("*", async (c, next) => { + c.set("tenant", { id: tenantId } as TenantEnv["Variables"]["tenant"]); + c.set("principal", { + id: "principal_test", + } as TenantEnv["Variables"]["principal"]); + await next(); + }); + outer.route("/api/tenants/:tenantId/artifacts", routes); + return outer; +} + +describe("createArtifactRoutes", () => { + test("lists artifacts for the tenant (happy path)", async () => { + const store = memoryStore({ + listByTenant: { + tenant_a: [ + listItem("art_1", "tenant_a"), + listItem("art_2", "tenant_a"), + ], + }, + details: {}, + }); + const app = appWith(store, "tenant_a"); + const res = await app.request("/api/tenants/tenant_a/artifacts"); + expect(res.status).toBe(200); + const body = (await res.json()) as ArtifactListPage; + expect(body.data).toHaveLength(2); + expect(body.data[0]?.id).toBe("art_1"); + expect(body.nextCursor).toBeNull(); + }); + + test("empty list returns data: []", async () => { + const store = memoryStore({ listByTenant: {}, details: {} }); + const app = appWith(store, "tenant_empty"); + const res = await app.request("/api/tenants/tenant_empty/artifacts"); + expect(res.status).toBe(200); + const body = (await res.json()) as ArtifactListPage; + expect(body.data).toEqual([]); + }); + + test("get returns the artifact body for the owning tenant", async () => { + const row = detail("art_9", "tenant_a"); + const store = memoryStore({ + listByTenant: {}, + details: { art_9: { tenantId: "tenant_a", row } }, + }); + const app = appWith(store, "tenant_a"); + const res = await app.request("/api/tenants/tenant_a/artifacts/art_9"); + expect(res.status).toBe(200); + const body = (await res.json()) as SerializedArtifact; + expect(body.id).toBe("art_9"); + expect(body.content).toBe("body of art_9"); + }); + + test("get returns 404 for a missing id", async () => { + const store = memoryStore({ listByTenant: {}, details: {} }); + const app = appWith(store, "tenant_a"); + const res = await app.request("/api/tenants/tenant_a/artifacts/missing"); + expect(res.status).toBe(404); + }); + + test("get returns 404 when the artifact belongs to another tenant", async () => { + const row = detail("art_x", "tenant_b"); + const store = memoryStore({ + listByTenant: {}, + details: { art_x: { tenantId: "tenant_b", row } }, + }); + // Request as tenant_a — store enforces tenant match. + const app = appWith(store, "tenant_a"); + const res = await app.request("/api/tenants/tenant_a/artifacts/art_x"); + expect(res.status).toBe(404); + }); +}); diff --git a/apps/hub/src/artifact-routes.ts b/apps/hub/src/artifact-routes.ts new file mode 100644 index 000000000..87abc234d --- /dev/null +++ b/apps/hub/src/artifact-routes.ts @@ -0,0 +1,115 @@ +/** + * Tenant-scoped Library L2 HTTP surface over the mounted `@corbits/artifacts` + * engine. List (newest-first, paginated) and get-by-id only — upload/search + * UI stays on later tickets. + * + * Authz uses the existing `asset` resource family so Library grants keep + * working without inventing a parallel vocabulary. + * + * The store is injected so tests can exercise happy/empty/cross-tenant + * without a live Postgres. + */ +import { + anonymousIdentity, + getArtifact, + listArtifacts, + serializeArtifact, + serializeArtifactListItem, + type ArtifactDb, + type SerializedArtifact, + type SerializedArtifactListItem, +} from "@corbits/artifacts"; +import type { RequireGrant, TenantEnv } from "@intx/hub-api"; +import { Hono } from "hono"; + +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; + +export type ArtifactListPage = { + readonly data: readonly SerializedArtifactListItem[]; + readonly nextCursor: string | null; +}; + +/** Minimal port the routes need — production wraps the engine db. */ +export type ArtifactRoutesStore = { + list( + tenantId: string, + opts: { limit: number; cursor: string | null }, + ): Promise; + get(tenantId: string, artifactId: string): Promise; +}; + +export type CreateArtifactRoutesDeps = { + store: ArtifactRoutesStore; + requireGrant: RequireGrant; +}; + +function parseLimit(raw: string | undefined): number { + if (raw === undefined || raw === "") return DEFAULT_LIMIT; + const n = Number.parseInt(raw, 10); + if (!Number.isFinite(n) || n < 1) return DEFAULT_LIMIT; + return Math.min(n, MAX_LIMIT); +} + +function parseCursor( + raw: string | undefined, +): { at: string; id: string } | undefined { + if (raw === undefined || raw === "") return undefined; + const sep = raw.lastIndexOf("__"); + if (sep <= 0 || sep === raw.length - 2) return undefined; + const at = raw.slice(0, sep); + const id = raw.slice(sep + 2); + if (!at || !id) return undefined; + return { at, id }; +} + +/** Production store over an artifacts engine db handle. */ +export function createArtifactDbStore(db: ArtifactDb): ArtifactRoutesStore { + return { + async list(tenantId, opts) { + const cursor = parseCursor(opts.cursor ?? undefined); + const result = await listArtifacts(db, anonymousIdentity, tenantId, { + limit: opts.limit, + ...(cursor !== undefined ? { cursor } : {}), + }); + return { + data: result.rows.map(serializeArtifactListItem), + nextCursor: result.nextCursor, + }; + }, + async get(tenantId, artifactId) { + const row = await getArtifact(db, artifactId); + if (row === null || row.tenantId !== tenantId) return null; + return serializeArtifact(row); + }, + }; +} + +export function createArtifactRoutes( + deps: CreateArtifactRoutesDeps, +): Hono { + const app = new Hono(); + + app.get("/", deps.requireGrant("asset:*", "read"), async (c) => { + const tenant = c.get("tenant"); + const limit = parseLimit(c.req.query("limit")); + const cursor = c.req.query("cursor") ?? null; + const page = await deps.store.list(tenant.id, { limit, cursor }); + return c.json(page); + }); + + app.get("/:artifactId", deps.requireGrant("asset:*", "read"), async (c) => { + const tenant = c.get("tenant"); + const artifactId = c.req.param("artifactId"); + const row = await deps.store.get(tenant.id, artifactId); + if (row === null) { + return c.json( + { error: { code: "not_found", message: "Artifact not found" } }, + 404, + ); + } + return c.json(row); + }); + + return app; +} diff --git a/apps/hub/src/artifacts-mount.ts b/apps/hub/src/artifacts-mount.ts index 40fb761c5..203449ee2 100644 --- a/apps/hub/src/artifacts-mount.ts +++ b/apps/hub/src/artifacts-mount.ts @@ -13,8 +13,8 @@ * 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. + * list/get routes live in `artifact-routes.ts` and are registered from + * the hub composition root when the mount succeeds. */ import { getLogger } from "@intx/log"; import { diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index de91e6e56..726aef4d6 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -59,6 +59,7 @@ import { createGitWorkflowPusher } from "@workbench/hub-client"; import { createOnboardingRoutes } from "@workbench/onboarding"; import { mountMemory } from "./memory-mount"; import { mountArtifacts } from "./artifacts-mount"; +import { createArtifactDbStore, createArtifactRoutes } from "./artifact-routes"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { type Context, type Next } from "hono"; @@ -495,9 +496,8 @@ export async function createHub(config: HubConfig) { // 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. + // `ARTIFACTS_DATABASE_URL` is unset. When mounted, tenant-scoped + // list + get routes serve Library L2 under `/artifacts`. // // The mount runs migrations against the configured DB; if the URL is // present but points at an unreachable/invalid cluster the migration @@ -513,9 +513,20 @@ export async function createHub(config: HubConfig) { ); artifactsHandle = undefined; } - log.info( - `Artifacts handle ${artifactsHandle !== undefined ? "available" : "unavailable (degraded mode)"}`, - ); + if (artifactsHandle !== undefined) { + app.route( + `${TENANT_PREFIX}/artifacts`, + createArtifactRoutes({ + store: createArtifactDbStore(artifactsHandle.db), + requireGrant: createRequireGrant({ + grantStore: chatGrantStore, + conditionRegistry: chatConditionRegistry, + }), + }), + ); + } else { + log.info("Artifacts handle unavailable (degraded mode)"); + } // Tells the signed-out screen which OAuth buttons to draw, without // exposing the credentials themselves — just which providers a full From d9dc25eb76dc3c1682661c1664d4bba1ba41c264 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 9 Aug 2026 17:17:51 -0700 Subject: [PATCH 5/7] Drop unused tenantId args from artifact route test helpers --- apps/hub/src/artifact-routes.test.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/hub/src/artifact-routes.test.ts b/apps/hub/src/artifact-routes.test.ts index 544d50017..efad463ef 100644 --- a/apps/hub/src/artifact-routes.test.ts +++ b/apps/hub/src/artifact-routes.test.ts @@ -9,10 +9,7 @@ import { } from "./artifact-routes"; import type { SerializedArtifact } from "@corbits/artifacts"; -function listItem( - id: string, - tenantId: string, -): ArtifactListPage["data"][number] { +function listItem(id: string): ArtifactListPage["data"][number] { return { id, kind: "document", @@ -27,9 +24,9 @@ function listItem( }; } -function detail(id: string, tenantId: string): SerializedArtifact { +function detail(id: string): SerializedArtifact { return { - ...listItem(id, tenantId), + ...listItem(id), content: `body of ${id}`, }; } @@ -77,10 +74,7 @@ describe("createArtifactRoutes", () => { test("lists artifacts for the tenant (happy path)", async () => { const store = memoryStore({ listByTenant: { - tenant_a: [ - listItem("art_1", "tenant_a"), - listItem("art_2", "tenant_a"), - ], + tenant_a: [listItem("art_1"), listItem("art_2")], }, details: {}, }); @@ -103,7 +97,7 @@ describe("createArtifactRoutes", () => { }); test("get returns the artifact body for the owning tenant", async () => { - const row = detail("art_9", "tenant_a"); + const row = detail("art_9"); const store = memoryStore({ listByTenant: {}, details: { art_9: { tenantId: "tenant_a", row } }, @@ -124,7 +118,7 @@ describe("createArtifactRoutes", () => { }); test("get returns 404 when the artifact belongs to another tenant", async () => { - const row = detail("art_x", "tenant_b"); + const row = detail("art_x"); const store = memoryStore({ listByTenant: {}, details: { art_x: { tenantId: "tenant_b", row } }, From 61ade301c3f55f2250bc5959d3cbdb0b94b234f0 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 9 Aug 2026 17:35:58 -0700 Subject: [PATCH 6/7] Document ARTIFACTS_DATABASE_URL and drop internal path comments Public-repo gate: env vars live in .env.example only; source comments must not reference private sibling repos. --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 95d6249a1..72f72c632 100644 --- a/.env.example +++ b/.env.example @@ -107,3 +107,8 @@ HUB_STATIC_DIR=../web/dist # EMBED_MODEL=text-embedding-3-small # EMBED_API_KEY= +# Optional artifacts plane (@corbits/artifacts). Must be the same Postgres +# cluster as the hub control plane (hard FKs into public.tenant/principal). +# Leave unset to boot without artifact persistence. +# ARTIFACTS_DATABASE_URL=postgres://workbench:workbench@localhost:5432/workbench + From 1f7eec57940ef53f6996064fb7e1b75c17af0094 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 9 Aug 2026 17:36:08 -0700 Subject: [PATCH 7/7] Drop private-repo path from artifacts-mount module docs --- apps/hub/src/artifacts-mount.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/hub/src/artifacts-mount.ts b/apps/hub/src/artifacts-mount.ts index 203449ee2..2d2f1b661 100644 --- a/apps/hub/src/artifacts-mount.ts +++ b/apps/hub/src/artifacts-mount.ts @@ -1,11 +1,10 @@ /** * 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. + * `@corbits/dock`'s `mountArtifacts`. `@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