diff --git a/docs/local-rip.md b/docs/local-rip.md index 59d632cf48..f881633075 100644 --- a/docs/local-rip.md +++ b/docs/local-rip.md @@ -105,9 +105,13 @@ The **assistant** default workflow pins the `@corbits/memory-tools` tool package (`workflows/assistant/src/index.ts`), and that pin only resolves once a `package-registry`-kind asset named `corbits-tools` carries its tarball (see `apps/hub/src/index.ts`'s `CORBITS_TOOLS_REGISTRY` comment). -Hub boot does not publish that asset. Onboarding and explicit -`@corbits/seeding` callers publish it via `@corbits/tool-registry-publish` -(bundles `@corbits/memory-tools` into a self-contained tarball and pushes it +Hub boot does not publish that asset, and neither does anything +hub-triggered (CL-8190): packing a package needs `fs` and `bun build`, +so `reconcileTenantDesiredState` reports an absent `workspace-pack` pin +`blocked` instead of packing it. The only publisher is the operator-run +`bun run publish-tools` (`scripts/publish-tools.ts`), an out-of-band API +client over `@corbits/tool-registry-publish` (bundles +`@corbits/memory-tools` into a self-contained tarball and pushes it through the hub's native asset REST routes). Descendants inherit it; `seedTenant` does not pack. Isolated tests run with no explicit tenant config so the walkthrough's personal diff --git a/docs/seed-reconciliation.md b/docs/seed-reconciliation.md index 752244ec3b..33af8368c5 100644 --- a/docs/seed-reconciliation.md +++ b/docs/seed-reconciliation.md @@ -133,11 +133,14 @@ first checking `GET /api/tenants/:id/skills/:name`. `publishCorbitsToolsRegistry` (`packages/tool-registry-publish/src/publish.ts`) finds-or-creates the tenant's `corbits-tools` package-registry asset, -then PUTs whatever tarball is missing. Onboarding and explicit -`@corbits/seeding` callers publish onto a tenant so descendants inherit -tarballs; `seedTenant` itself does not pack. Hub boot does not publish. -The operator-facing install path is `bun run publish-tools` -(`scripts/publish-tools.ts`), which signs an existing admin in and +then PUTs whatever tarball is missing. Packing a package needs `fs` and +`bun build`, so nothing hub-triggered may call it (CL-8190): hub boot +never publishes, `seedTenant` never packs, and +`reconcileTenantDesiredState` (`packages/onboarding/src/desired-state.ts`) +reports an absent `workspace-pack` tool pin `blocked` rather than +publishing it. The only caller of `publishCorbitsToolsRegistry` is the +operator-facing `bun run publish-tools` (`scripts/publish-tools.ts`), a +plain out-of-band API client: it signs an existing admin in and publishes onto an already-existing tenant — the same find-or-create, 409-tolerant asset flow, never a new provisioning path. Each packed tarball's `package.json` also carries the `ToolSurfaceManifest` the @@ -265,7 +268,10 @@ nothing seeded from hub boot. `reconcileTenantDesiredState` is the only installer. It reads the tenant's real state (native GETs) and installs ONLY absent pins; with everything present it is a read-only pass — `seedTenant` is -never entered, the registry publish is gated on the seeded check, +never entered. An absent `workspace-pack` tool pin is never packed or +published here (see "Tool registry publish" above): it reports +`blocked`, and an operator installs it with `bun run publish-tools`. A +`tarball-url` pin is still installed here — that's a plain fetch + PUT, and a tarball already published under its `name@version` is skipped. Sidecar-unavailable failures report `blocked` (the same class `ensureSeeded` treats as pending-agents); other failures report diff --git a/packages/onboarding/src/desired-state.ts b/packages/onboarding/src/desired-state.ts index 752b6e6b2c..0bf8d2a505 100644 --- a/packages/onboarding/src/desired-state.ts +++ b/packages/onboarding/src/desired-state.ts @@ -31,13 +31,11 @@ import { type DefaultWorkflow, type ModelSource, type SeedTenantArgs, - type ToolRegistryPublisher, } from "./tenant-seed"; import { fetchRegistryTarballSource, installRegistryTarball, isCorbitsToolsRegistrySeeded, - publishCorbitsToolsRegistry, REQUIRED_SEED_TOOL_PACKAGES, } from "@corbits/tool-registry-publish"; import type { WorkflowPusher } from "@corbits/connections/workflow-push"; @@ -322,8 +320,6 @@ export type ReconcileArgs = { }; model: ModelSource | undefined; pushWorkflow: WorkflowPusher; - /** Defaults to the real `publishCorbitsToolsRegistry`. */ - publishToolRegistry?: ToolRegistryPublisher; /** Test seam standing in for the deploy step, the same way * `ensureSeeded` accepts one. */ seedTenantFn?: (args: SeedTenantArgs) => ReturnType; @@ -426,15 +422,20 @@ export async function resolveTenantDeployer( /** * Installs ONLY the absent pins, tools first, then skills + grants + * workflows together through `seedTenant`. Safe to re-run: with every - * pin present this is READS ONLY — `seedTenant` is never entered, the - * publish is gated on the registry not already seeded, and a tarball - * already published under its name@version is skipped (immutable). - * Sidecar-unavailable (502-class) pins report `blocked` without - * throwing — the same class `ensureSeeded` treats as pending; any other - * failure reports `failed` and is safe to re-run. Skill pins are - * re-read after the seed pass: still-absent (stock cutover — the skills - * mount is gone, so the 404-tolerant plant writes nothing) reports - * `blocked`, never `installed`. + * pin present this is READS ONLY — `seedTenant` is never entered. + * `workspace-pack` tool pins are never packed or published here (CL-8190: + * that requires `fs` + `bun build`, which no hub-triggered path may run — + * an operator publishes them out of band with `bun run publish-tools`); + * an absent workspace-pack pin reports `blocked` and this function stays + * read-only for tools. A `tarball-url` pin is still installed here — that + * install is a plain fetch + upload, and a tarball already published + * under its name@version is skipped (immutable). Sidecar-unavailable + * (502-class) pins report `blocked` without throwing — the same class + * `ensureSeeded` treats as pending; any other failure reports `failed` + * and is safe to re-run. Skill pins are re-read after the seed pass: + * still-absent (stock cutover — the skills mount is gone, so the + * 404-tolerant plant writes nothing) reports `blocked`, never + * `installed`. */ export async function reconcileTenantDesiredState( args: ReconcileArgs, @@ -458,26 +459,18 @@ export async function reconcileTenantDesiredState( } } else { try { + // `workspace-pack` pins are never packed or published from here + // (CL-8190): packing needs `fs` + `bun build`, which no + // hub-triggered reconcile path may run. An absent pin just reports + // `blocked` — the operator installs it out of band with + // `bun run publish-tools`. const workspacePacks = TENANT_DESIRED_STATE.toolPackages.filter( (pin) => pin.source.kind === "workspace-pack", ); - if (workspacePacks.length > 0) { - const publish = args.publishToolRegistry ?? publishCorbitsToolsRegistry; - await publish({ - api, - cookies, - hubUrl: args.hubUrl, - tenantId, - log, - }); - for (const pin of workspacePacks) { - pins.push({ - name: pin.name, - kind: "tool-package", - status: "installed", - }); - } + for (const pin of workspacePacks) { + pins.push({ name: pin.name, kind: "tool-package", status: "blocked" }); } + if (workspacePacks.length > 0) sawBlocked = true; // The tarball-url half of the tool install: live and wired, but // data-gated — no `tarball-url` pin exists in // `TENANT_DESIRED_STATE` yet (no external artifacts exist), so this diff --git a/packages/onboarding/src/tenant-seed.ts b/packages/onboarding/src/tenant-seed.ts index 6e1d5ab9c2..8a2c19b697 100644 --- a/packages/onboarding/src/tenant-seed.ts +++ b/packages/onboarding/src/tenant-seed.ts @@ -96,7 +96,6 @@ import { serializeDiligenceBriefWorkflow, } from "@corbits/diligence-brief-workflow"; import { WORKFLOW_CATALOG } from "@corbits/workflows/catalog"; -import { type PublishCorbitsToolsRegistryArgs } from "@corbits/tool-registry-publish"; import { WORKFLOW_SOURCE_ENTRY } from "@corbits/workflows"; import { HubApiError, @@ -1124,18 +1123,6 @@ export type SeedTenant = { domain: string; }; -/** - * Publishes a tenant's `corbits-tools` package-registry asset. Defaults - * to the real `publishCorbitsToolsRegistry`; a test double can replace - * it so a unit test never bundles a real tarball or dials the hub's - * tarball REST routes, the same way `pushWorkflow` replaces the real - * git push. Used by `workbench setup` (the root tenant), not by - * `seedTenant`. - */ -export type ToolRegistryPublisher = ( - args: Omit, -) => Promise; - export type SeedTenantArgs = { api: ApiCall; cookies: string[]; diff --git a/packages/onboarding/test/desired-state-reconcile.test.ts b/packages/onboarding/test/desired-state-reconcile.test.ts index b9f4185954..19ce29c43e 100644 --- a/packages/onboarding/test/desired-state-reconcile.test.ts +++ b/packages/onboarding/test/desired-state-reconcile.test.ts @@ -49,7 +49,6 @@ type Stub = { function harness(state: Stub) { const calls: { method: string; path: string }[] = []; let seedTenantCalls = 0; - let published = 0; const api = (async (method: string, path: string): Promise => { calls.push({ method, path }); if ( @@ -162,12 +161,6 @@ function harness(state: Stub) { throw new Error(`stub api: unhandled ${method} ${path}`); }) as unknown as ApiCall; - const publishToolRegistry = async () => { - calls.push({ method: "PUBLISH", path: "corbits-tools" }); - published += 1; - state.registryTarballs = true; - }; - const pushWorkflow: WorkflowPusher = async (args) => { calls.push({ method: "PUSH", path: args.remoteUrl }); return { outcome: "pushed", commitSha: "a".repeat(40) }; @@ -180,7 +173,6 @@ function harness(state: Stub) { tenant: { tenantId: TENANT_ID, principalId: "prn_1", domain: "t.local" }, model: MODEL, pushWorkflow, - publishToolRegistry, seedTenantFn: async (seedArgs) => { calls.push({ method: "SEED_TENANT", path: seedArgs.tenant.tenantId }); seedTenantCalls += 1; @@ -197,12 +189,11 @@ function harness(state: Stub) { calls, nonGetCalls: () => calls.filter((c) => c.method !== "GET"), seedTenantCalls: () => seedTenantCalls, - publishedCount: () => published, }; } describe("reconcileTenantDesiredState", () => { - test("a fresh tenant installs every pin: tools first, then one seedTenant", async () => { + test("a fresh tenant reports absent workspace-pack tools blocked (CL-8190: never packed here) while workflows still install", async () => { const h = harness({ workflowAssets: false, liveDeployments: false, @@ -211,11 +202,11 @@ describe("reconcileTenantDesiredState", () => { catalogOfferings: true, }); const report = await reconcileTenantDesiredState(h.args); - expect(report.ready).toBe(true); - expect(h.publishedCount()).toBe(1); + expect(report.ready).toBe(false); expect(h.seedTenantCalls()).toBe(1); const tools = report.pins.filter((p) => p.kind === "tool-package"); - expect(tools.every((p) => p.status === "installed")).toBe(true); + expect(tools.every((p) => p.status === "blocked")).toBe(true); + expect(h.nonGetCalls().some((c) => c.method === "PUBLISH")).toBe(false); expect(report.pins.filter((p) => p.kind === "workflow")).toEqual( TENANT_DESIRED_STATE.workflows.map((w) => ({ name: w.assetName, @@ -241,14 +232,15 @@ describe("reconcileTenantDesiredState", () => { const first = await reconcileTenantDesiredState(h.args); expect(first.ready).toBe(true); expect(h.seedTenantCalls()).toBe(0); - expect(h.publishedCount()).toBe(0); expect(h.nonGetCalls().length).toBe(0); - // And a fresh install followed by a revisit behaves identically. + // And a fresh workflow/skill install (tools already present, as an + // operator's out-of-band `bun run publish-tools` would leave them) + // followed by a revisit behaves identically. const h2 = harness({ workflowAssets: false, liveDeployments: false, - registryTarballs: false, + registryTarballs: true, skills: false, catalogOfferings: true, });