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
10 changes: 7 additions & 3 deletions docs/local-rip.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions docs/seed-reconciliation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
51 changes: 22 additions & 29 deletions packages/onboarding/src/desired-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<typeof seedTenant>;
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
13 changes: 0 additions & 13 deletions packages/onboarding/src/tenant-seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<PublishCorbitsToolsRegistryArgs, "fetchImpl">,
) => Promise<unknown>;

export type SeedTenantArgs = {
api: ApiCall;
cookies: string[];
Expand Down
24 changes: 8 additions & 16 deletions packages/onboarding/test/desired-state-reconcile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown> => {
calls.push({ method, path });
if (
Expand Down Expand Up @@ -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) };
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
});
Expand Down
Loading