Skip to content

Commit bfedcae

Browse files
feat(hub)!: tool tarballs are published by a client script, never at hub boot (CL-8190) (#776)
* Add tests for reconcile never packing tool tarballs (CL-8190) * onboarding: reconcile reports missing tool packages blocked, never packs (CL-8190) * Update docs: tool tarballs are published only by bun run publish-tools
1 parent 3ff5570 commit bfedcae

5 files changed

Lines changed: 49 additions & 67 deletions

File tree

‎docs/local-rip.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,13 @@ The **assistant** default workflow pins the `@corbits/memory-tools` tool
105105
package (`workflows/assistant/src/index.ts`), and that pin only resolves
106106
once a `package-registry`-kind asset named `corbits-tools` carries its
107107
tarball (see `apps/hub/src/index.ts`'s `CORBITS_TOOLS_REGISTRY` comment).
108-
Hub boot does not publish that asset. Onboarding and explicit
109-
`@corbits/seeding` callers publish it via `@corbits/tool-registry-publish`
110-
(bundles `@corbits/memory-tools` into a self-contained tarball and pushes it
108+
Hub boot does not publish that asset, and neither does anything
109+
hub-triggered (CL-8190): packing a package needs `fs` and `bun build`,
110+
so `reconcileTenantDesiredState` reports an absent `workspace-pack` pin
111+
`blocked` instead of packing it. The only publisher is the operator-run
112+
`bun run publish-tools` (`scripts/publish-tools.ts`), an out-of-band API
113+
client over `@corbits/tool-registry-publish` (bundles
114+
`@corbits/memory-tools` into a self-contained tarball and pushes it
111115
through the hub's native asset REST routes). Descendants inherit it;
112116
`seedTenant` does not pack. Isolated
113117
tests run with no explicit tenant config so the walkthrough's personal

‎docs/seed-reconciliation.md‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,14 @@ first checking `GET /api/tenants/:id/skills/:name`.
133133

134134
`publishCorbitsToolsRegistry` (`packages/tool-registry-publish/src/publish.ts`)
135135
finds-or-creates the tenant's `corbits-tools` package-registry asset,
136-
then PUTs whatever tarball is missing. Onboarding and explicit
137-
`@corbits/seeding` callers publish onto a tenant so descendants inherit
138-
tarballs; `seedTenant` itself does not pack. Hub boot does not publish.
139-
The operator-facing install path is `bun run publish-tools`
140-
(`scripts/publish-tools.ts`), which signs an existing admin in and
136+
then PUTs whatever tarball is missing. Packing a package needs `fs` and
137+
`bun build`, so nothing hub-triggered may call it (CL-8190): hub boot
138+
never publishes, `seedTenant` never packs, and
139+
`reconcileTenantDesiredState` (`packages/onboarding/src/desired-state.ts`)
140+
reports an absent `workspace-pack` tool pin `blocked` rather than
141+
publishing it. The only caller of `publishCorbitsToolsRegistry` is the
142+
operator-facing `bun run publish-tools` (`scripts/publish-tools.ts`), a
143+
plain out-of-band API client: it signs an existing admin in and
141144
publishes onto an already-existing tenant — the same find-or-create,
142145
409-tolerant asset flow, never a new provisioning path. Each packed
143146
tarball's `package.json` also carries the `ToolSurfaceManifest` the
@@ -265,7 +268,10 @@ nothing seeded from hub boot.
265268
`reconcileTenantDesiredState` is the only installer. It reads the
266269
tenant's real state (native GETs) and installs ONLY absent pins;
267270
with everything present it is a read-only pass — `seedTenant` is
268-
never entered, the registry publish is gated on the seeded check,
271+
never entered. An absent `workspace-pack` tool pin is never packed or
272+
published here (see "Tool registry publish" above): it reports
273+
`blocked`, and an operator installs it with `bun run publish-tools`. A
274+
`tarball-url` pin is still installed here — that's a plain fetch + PUT,
269275
and a tarball already published under its `name@version` is skipped.
270276
Sidecar-unavailable failures report `blocked` (the same class
271277
`ensureSeeded` treats as pending-agents); other failures report

‎packages/onboarding/src/desired-state.ts‎

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,11 @@ import {
3131
type DefaultWorkflow,
3232
type ModelSource,
3333
type SeedTenantArgs,
34-
type ToolRegistryPublisher,
3534
} from "./tenant-seed";
3635
import {
3736
fetchRegistryTarballSource,
3837
installRegistryTarball,
3938
isCorbitsToolsRegistrySeeded,
40-
publishCorbitsToolsRegistry,
4139
REQUIRED_SEED_TOOL_PACKAGES,
4240
} from "@corbits/tool-registry-publish";
4341
import type { WorkflowPusher } from "@corbits/connections/workflow-push";
@@ -322,8 +320,6 @@ export type ReconcileArgs = {
322320
};
323321
model: ModelSource | undefined;
324322
pushWorkflow: WorkflowPusher;
325-
/** Defaults to the real `publishCorbitsToolsRegistry`. */
326-
publishToolRegistry?: ToolRegistryPublisher;
327323
/** Test seam standing in for the deploy step, the same way
328324
* `ensureSeeded` accepts one. */
329325
seedTenantFn?: (args: SeedTenantArgs) => ReturnType<typeof seedTenant>;
@@ -426,15 +422,20 @@ export async function resolveTenantDeployer(
426422
/**
427423
* Installs ONLY the absent pins, tools first, then skills + grants +
428424
* workflows together through `seedTenant`. Safe to re-run: with every
429-
* pin present this is READS ONLY — `seedTenant` is never entered, the
430-
* publish is gated on the registry not already seeded, and a tarball
431-
* already published under its name@version is skipped (immutable).
432-
* Sidecar-unavailable (502-class) pins report `blocked` without
433-
* throwing — the same class `ensureSeeded` treats as pending; any other
434-
* failure reports `failed` and is safe to re-run. Skill pins are
435-
* re-read after the seed pass: still-absent (stock cutover — the skills
436-
* mount is gone, so the 404-tolerant plant writes nothing) reports
437-
* `blocked`, never `installed`.
425+
* pin present this is READS ONLY — `seedTenant` is never entered.
426+
* `workspace-pack` tool pins are never packed or published here (CL-8190:
427+
* that requires `fs` + `bun build`, which no hub-triggered path may run —
428+
* an operator publishes them out of band with `bun run publish-tools`);
429+
* an absent workspace-pack pin reports `blocked` and this function stays
430+
* read-only for tools. A `tarball-url` pin is still installed here — that
431+
* install is a plain fetch + upload, and a tarball already published
432+
* under its name@version is skipped (immutable). Sidecar-unavailable
433+
* (502-class) pins report `blocked` without throwing — the same class
434+
* `ensureSeeded` treats as pending; any other failure reports `failed`
435+
* and is safe to re-run. Skill pins are re-read after the seed pass:
436+
* still-absent (stock cutover — the skills mount is gone, so the
437+
* 404-tolerant plant writes nothing) reports `blocked`, never
438+
* `installed`.
438439
*/
439440
export async function reconcileTenantDesiredState(
440441
args: ReconcileArgs,
@@ -458,26 +459,18 @@ export async function reconcileTenantDesiredState(
458459
}
459460
} else {
460461
try {
462+
// `workspace-pack` pins are never packed or published from here
463+
// (CL-8190): packing needs `fs` + `bun build`, which no
464+
// hub-triggered reconcile path may run. An absent pin just reports
465+
// `blocked` — the operator installs it out of band with
466+
// `bun run publish-tools`.
461467
const workspacePacks = TENANT_DESIRED_STATE.toolPackages.filter(
462468
(pin) => pin.source.kind === "workspace-pack",
463469
);
464-
if (workspacePacks.length > 0) {
465-
const publish = args.publishToolRegistry ?? publishCorbitsToolsRegistry;
466-
await publish({
467-
api,
468-
cookies,
469-
hubUrl: args.hubUrl,
470-
tenantId,
471-
log,
472-
});
473-
for (const pin of workspacePacks) {
474-
pins.push({
475-
name: pin.name,
476-
kind: "tool-package",
477-
status: "installed",
478-
});
479-
}
470+
for (const pin of workspacePacks) {
471+
pins.push({ name: pin.name, kind: "tool-package", status: "blocked" });
480472
}
473+
if (workspacePacks.length > 0) sawBlocked = true;
481474
// The tarball-url half of the tool install: live and wired, but
482475
// data-gated — no `tarball-url` pin exists in
483476
// `TENANT_DESIRED_STATE` yet (no external artifacts exist), so this

‎packages/onboarding/src/tenant-seed.ts‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ import {
9696
serializeDiligenceBriefWorkflow,
9797
} from "@corbits/diligence-brief-workflow";
9898
import { WORKFLOW_CATALOG } from "@corbits/workflows/catalog";
99-
import { type PublishCorbitsToolsRegistryArgs } from "@corbits/tool-registry-publish";
10099
import { WORKFLOW_SOURCE_ENTRY } from "@corbits/workflows";
101100
import {
102101
HubApiError,
@@ -1124,18 +1123,6 @@ export type SeedTenant = {
11241123
domain: string;
11251124
};
11261125

1127-
/**
1128-
* Publishes a tenant's `corbits-tools` package-registry asset. Defaults
1129-
* to the real `publishCorbitsToolsRegistry`; a test double can replace
1130-
* it so a unit test never bundles a real tarball or dials the hub's
1131-
* tarball REST routes, the same way `pushWorkflow` replaces the real
1132-
* git push. Used by `workbench setup` (the root tenant), not by
1133-
* `seedTenant`.
1134-
*/
1135-
export type ToolRegistryPublisher = (
1136-
args: Omit<PublishCorbitsToolsRegistryArgs, "fetchImpl">,
1137-
) => Promise<unknown>;
1138-
11391126
export type SeedTenantArgs = {
11401127
api: ApiCall;
11411128
cookies: string[];

‎packages/onboarding/test/desired-state-reconcile.test.ts‎

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ type Stub = {
4949
function harness(state: Stub) {
5050
const calls: { method: string; path: string }[] = [];
5151
let seedTenantCalls = 0;
52-
let published = 0;
5352
const api = (async (method: string, path: string): Promise<unknown> => {
5453
calls.push({ method, path });
5554
if (
@@ -162,12 +161,6 @@ function harness(state: Stub) {
162161
throw new Error(`stub api: unhandled ${method} ${path}`);
163162
}) as unknown as ApiCall;
164163

165-
const publishToolRegistry = async () => {
166-
calls.push({ method: "PUBLISH", path: "corbits-tools" });
167-
published += 1;
168-
state.registryTarballs = true;
169-
};
170-
171164
const pushWorkflow: WorkflowPusher = async (args) => {
172165
calls.push({ method: "PUSH", path: args.remoteUrl });
173166
return { outcome: "pushed", commitSha: "a".repeat(40) };
@@ -180,7 +173,6 @@ function harness(state: Stub) {
180173
tenant: { tenantId: TENANT_ID, principalId: "prn_1", domain: "t.local" },
181174
model: MODEL,
182175
pushWorkflow,
183-
publishToolRegistry,
184176
seedTenantFn: async (seedArgs) => {
185177
calls.push({ method: "SEED_TENANT", path: seedArgs.tenant.tenantId });
186178
seedTenantCalls += 1;
@@ -197,12 +189,11 @@ function harness(state: Stub) {
197189
calls,
198190
nonGetCalls: () => calls.filter((c) => c.method !== "GET"),
199191
seedTenantCalls: () => seedTenantCalls,
200-
publishedCount: () => published,
201192
};
202193
}
203194

204195
describe("reconcileTenantDesiredState", () => {
205-
test("a fresh tenant installs every pin: tools first, then one seedTenant", async () => {
196+
test("a fresh tenant reports absent workspace-pack tools blocked (CL-8190: never packed here) while workflows still install", async () => {
206197
const h = harness({
207198
workflowAssets: false,
208199
liveDeployments: false,
@@ -211,11 +202,11 @@ describe("reconcileTenantDesiredState", () => {
211202
catalogOfferings: true,
212203
});
213204
const report = await reconcileTenantDesiredState(h.args);
214-
expect(report.ready).toBe(true);
215-
expect(h.publishedCount()).toBe(1);
205+
expect(report.ready).toBe(false);
216206
expect(h.seedTenantCalls()).toBe(1);
217207
const tools = report.pins.filter((p) => p.kind === "tool-package");
218-
expect(tools.every((p) => p.status === "installed")).toBe(true);
208+
expect(tools.every((p) => p.status === "blocked")).toBe(true);
209+
expect(h.nonGetCalls().some((c) => c.method === "PUBLISH")).toBe(false);
219210
expect(report.pins.filter((p) => p.kind === "workflow")).toEqual(
220211
TENANT_DESIRED_STATE.workflows.map((w) => ({
221212
name: w.assetName,
@@ -241,14 +232,15 @@ describe("reconcileTenantDesiredState", () => {
241232
const first = await reconcileTenantDesiredState(h.args);
242233
expect(first.ready).toBe(true);
243234
expect(h.seedTenantCalls()).toBe(0);
244-
expect(h.publishedCount()).toBe(0);
245235
expect(h.nonGetCalls().length).toBe(0);
246236

247-
// And a fresh install followed by a revisit behaves identically.
237+
// And a fresh workflow/skill install (tools already present, as an
238+
// operator's out-of-band `bun run publish-tools` would leave them)
239+
// followed by a revisit behaves identically.
248240
const h2 = harness({
249241
workflowAssets: false,
250242
liveDeployments: false,
251-
registryTarballs: false,
243+
registryTarballs: true,
252244
skills: false,
253245
catalogOfferings: true,
254246
});

0 commit comments

Comments
 (0)