diff --git a/apps/hub/package.json b/apps/hub/package.json index fc79a5ae46..23de6c59de 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -15,7 +15,6 @@ "@chat-adapter/state-memory": "^4.35.0", "@corbits/agent-directory": "workspace:*", "@corbits/approvals": "workspace:*", - "@corbits/artifact-ui": "workspace:*", "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#5309a15d4aa504f132c54b4da08bb672b4903873", "@corbits/codex-provider": "github:corbitsdev/corbits-codex-provider#a1f475f341261c05fa8dade0c82f1246c175b5b3", "@corbits/cron": "workspace:*", @@ -23,7 +22,6 @@ "@corbits/inference-catalog": "workspace:*", "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913", - "@corbits/tool-registry-publish": "workspace:*", "@corbits/url-path": "workspace:*", "@corbits/webhook-triggers": "workspace:*", "@corbits/workflows": "workspace:*", diff --git a/apps/hub/src/library-kind-segments.ts b/apps/hub/src/library-kind-segments.ts new file mode 100644 index 0000000000..45f06ac915 --- /dev/null +++ b/apps/hub/src/library-kind-segments.ts @@ -0,0 +1,40 @@ +// The hub's own copy of the Files kind-nav mapping, kept in step with +// `apps/web/src/library/kind-filter.ts` by hand: it buckets the same +// `ArtifactListRow`s `@corbits/artifacts`' `countArtifactsBySegments` +// walks, so the per-kind counts the web app's kind nav shows match what +// the web app itself filters client-side. + +import type { ArtifactListRow } from "@corbits/artifacts"; + +export const LIBRARY_KIND_SEGMENTS = ["document", "sheet", "pdf", "routine"] as const; + +function titleExtension(title: string): string { + const lower = title.trim().toLowerCase(); + const dot = lower.lastIndexOf("."); + if (dot <= 0 || dot === lower.length - 1) return ""; + return lower.slice(dot + 1); +} + +export function artifactMatchesLibraryKindSegment( + artifact: Pick, + segment: string, +): boolean { + const kind = artifact.kind.trim().toLowerCase(); + const ext = titleExtension(artifact.title); + + switch (segment) { + case "document": + if (kind === "document") return true; + return kind === "file" && (ext === "doc" || ext === "txt" || ext === "md"); + case "sheet": + if (kind === "csv-export" || kind === "sheet") return true; + return kind === "file" && (ext === "xls" || ext === "csv"); + case "pdf": + if (kind === "pdf") return true; + return kind === "file" && ext === "pdf"; + case "routine": + return kind === "routine"; + default: + return false; + } +} diff --git a/apps/hub/src/server.ts b/apps/hub/src/server.ts index 15d43059c4..c025521ef0 100644 --- a/apps/hub/src/server.ts +++ b/apps/hub/src/server.ts @@ -88,10 +88,7 @@ import { mountWorkflowArtifacts, type WorkflowArtifactEnv, } from "@corbits/artifacts"; -import { - artifactMatchesLibraryKindSegment, - LIBRARY_KIND_SEGMENTS, -} from "@corbits/artifact-ui/kind-filter"; +import { artifactMatchesLibraryKindSegment, LIBRARY_KIND_SEGMENTS } from "./library-kind-segments"; import type { DB } from "@intx/db"; import { sidecar, workflowRun } from "@intx/db/schema"; import path from "node:path"; diff --git a/apps/web/package.json b/apps/web/package.json index 8c84aa35da..25b09d158d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,7 +19,6 @@ "dependencies": { "@corbits/agent-directory": "workspace:*", "@corbits/approvals": "workspace:*", - "@corbits/artifact-ui": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/myra": "workspace:*", "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", diff --git a/apps/web/src/library/artifact-card.tsx b/apps/web/src/library/artifact-card.tsx index 91dcb1ee96..b5490cf2a4 100644 --- a/apps/web/src/library/artifact-card.tsx +++ b/apps/web/src/library/artifact-card.tsx @@ -5,7 +5,7 @@ import { Badge, artifactKindLabel, formatRelativeTime } from "@corbits/react-ui"; import type { ReactNode } from "react"; -import type { ArtifactSummary } from "@corbits/artifact-ui/types"; +import type { ArtifactSummary } from "./artifact-summary"; export type ArtifactCardMeta = { readonly sourceChannel?: string | null; diff --git a/packages/artifact-ui/src/types.ts b/apps/web/src/library/artifact-summary.ts similarity index 100% rename from packages/artifact-ui/src/types.ts rename to apps/web/src/library/artifact-summary.ts diff --git a/apps/web/src/library/index.ts b/apps/web/src/library/index.ts index bae14b6b83..f335e0a7c1 100644 --- a/apps/web/src/library/index.ts +++ b/apps/web/src/library/index.ts @@ -1,4 +1,4 @@ -export * from "@corbits/artifact-ui/types"; +export * from "./artifact-summary"; export { sortArtifacts, filterArtifacts } from "./sort-filter"; export type { ArtifactSort } from "./sort-filter"; export { ArtifactCard } from "./artifact-card"; @@ -9,8 +9,8 @@ export { libraryKindSegmentFromPath, libraryArtifactPath, libraryArtifactIdFromPath, -} from "@corbits/artifact-ui/kind-filter"; -export type { LibraryKindSegment } from "@corbits/artifact-ui/kind-filter"; +} from "./kind-filter"; +export type { LibraryKindSegment } from "./kind-filter"; export { workflowRunIdFromSource } from "./provenance"; export { ARTIFACT_RENDERER_KINDS, diff --git a/packages/artifact-ui/src/kind-filter.test.ts b/apps/web/src/library/kind-filter.test.ts similarity index 100% rename from packages/artifact-ui/src/kind-filter.test.ts rename to apps/web/src/library/kind-filter.test.ts diff --git a/packages/artifact-ui/src/kind-filter.ts b/apps/web/src/library/kind-filter.ts similarity index 90% rename from packages/artifact-ui/src/kind-filter.ts rename to apps/web/src/library/kind-filter.ts index 696e189223..ae93a882a1 100644 --- a/packages/artifact-ui/src/kind-filter.ts +++ b/apps/web/src/library/kind-filter.ts @@ -1,16 +1,13 @@ // Pure helpers for the Files kind nav (`/files`, `/files/document`, …) — -// this package's own names keep the "library" vocabulary (it's the +// this module's own names keep the "library" vocabulary (it's the // internal name for the artifact store), but `LIBRARY_PATH` tracks the // app's real mount point: `/library` stays routable only as a redirect // to `/files` — see `legacy-settings-redirects.tsx`. -// Framework-free so both the web app (rendering the nav + filtering the -// visible list) and the hub (computing honest per-kind counts server-side) -// share one mapping instead of two copies drifting apart. import { decodedOrNull } from "@corbits/url-path"; import { titleExtension } from "./title-extension"; -import type { ArtifactSummary } from "./types"; +import type { ArtifactSummary } from "./artifact-summary"; const LIBRARY_PATH = "/files"; diff --git a/apps/web/src/library/renderer-kind.ts b/apps/web/src/library/renderer-kind.ts index e50062f847..985b3aec50 100644 --- a/apps/web/src/library/renderer-kind.ts +++ b/apps/web/src/library/renderer-kind.ts @@ -4,7 +4,7 @@ // the renderer itself never has to know whether it was picked by a Library // `kind` string or a chat `Part`'s MIME type. -import { titleExtension } from "@corbits/artifact-ui/title-extension"; +import { titleExtension } from "./title-extension"; export const ARTIFACT_RENDERER_KINDS = ["doc", "sheet", "pdf", "html", "unsupported"] as const; diff --git a/apps/web/src/library/sort-filter.ts b/apps/web/src/library/sort-filter.ts index 0e953f7420..272cff3e0a 100644 --- a/apps/web/src/library/sort-filter.ts +++ b/apps/web/src/library/sort-filter.ts @@ -1,4 +1,4 @@ -import type { ArtifactSummary } from "@corbits/artifact-ui/types"; +import type { ArtifactSummary } from "./artifact-summary"; export type ArtifactSort = "newest" | "oldest"; diff --git a/packages/artifact-ui/src/title-extension.ts b/apps/web/src/library/title-extension.ts similarity index 100% rename from packages/artifact-ui/src/title-extension.ts rename to apps/web/src/library/title-extension.ts diff --git a/packages/tool-registry-publish/src/tool-name-limits.test.ts b/apps/web/src/tools/registry-publish.test.ts similarity index 63% rename from packages/tool-registry-publish/src/tool-name-limits.test.ts rename to apps/web/src/tools/registry-publish.test.ts index 25d4715675..86075bf8fc 100644 --- a/packages/tool-registry-publish/src/tool-name-limits.test.ts +++ b/apps/web/src/tools/registry-publish.test.ts @@ -6,8 +6,9 @@ // package's names are checked here against the strictest limit. import { describe, expect, test } from "bun:test"; import path from "node:path"; +import { existsSync } from "node:fs"; +import { readdir } from "node:fs/promises"; import { encodeToolName } from "@intx/inference"; -import { CORBITS_TOOL_PACKAGE_DIRS } from "./registry"; const OPENAI_LIMIT = { provider: "openai", maxLength: 64 } as const; @@ -24,10 +25,27 @@ function isBundle(value: unknown): value is Bundle { ); } +const TOOLS_ROOT = path.resolve(import.meta.dir, "../../../../tools"); + +async function toolPackageDirs(): Promise { + const entries = await readdir(TOOLS_ROOT, { withFileTypes: true }); + return entries + .filter( + (entry) => + entry.isDirectory() && existsSync(path.join(TOOLS_ROOT, entry.name, "package.json")), + ) + .map((entry) => entry.name); +} + describe("corbits tool packages fit the 64-char OpenAI-compatible tool-name cap", () => { - for (const dir of CORBITS_TOOL_PACKAGE_DIRS) { - test(path.basename(dir), async () => { - const mod = (await import(path.join(dir, "src", "index.ts"))) as Record; + test("every tools/* package's qualified tool names encode within the cap", async () => { + const dirs = await toolPackageDirs(); + expect(dirs.length).toBeGreaterThan(0); + for (const dir of dirs) { + const mod = (await import(path.join(TOOLS_ROOT, dir, "src", "index.ts"))) as Record< + string, + unknown + >; const bundles = Object.values(mod).filter(isBundle); expect(bundles.length).toBeGreaterThan(0); for (const bundle of bundles) { @@ -36,6 +54,6 @@ describe("corbits tool packages fit the 64-char OpenAI-compatible tool-name cap" expect(() => encodeToolName(qualified, OPENAI_LIMIT)).not.toThrow(); } } - }); - } + } + }); }); diff --git a/apps/web/src/tools/registry-publish.ts b/apps/web/src/tools/registry-publish.ts new file mode 100644 index 0000000000..abab8a6f78 --- /dev/null +++ b/apps/web/src/tools/registry-publish.ts @@ -0,0 +1,218 @@ +// Publishes every `tools/*` package's raw TypeScript source as an +// npm-shaped tarball into the tenant's `corbits-tools` package-registry +// asset — the same ensure-asset-then-PUT-tarball plumbing +// `myra-deploy.ts` already uses for Myra's own workflow source, and the +// same `packTarball` (Interchange's stock tool-package resolver installs +// and runs a tool package straight from its packed source, so no bundling +// step is needed here either). `tools/*` directories are read directly +// off the workspace at build time via `import.meta.glob`, so a new +// `tools/*` package is picked up without editing a directory list here. +import { type } from "arktype"; + +import { packTarball } from "../myra-deploy"; + +export const CORBITS_TOOLS_REGISTRY = "corbits-tools"; + +class RegistryPublishError extends Error {} + +async function readErrorBody(response: Response): Promise { + const body: unknown = await response.json().catch(() => undefined); + const envelope = type({ + error: { code: "string", userMessage: "string", refId: "string" }, + })(body); + return envelope instanceof type.errors ? `HTTP ${response.status}` : envelope.error.userMessage; +} + +const AssetCreatedShape = type({ id: "string" }); +const AssetListShape = type({ id: "string", name: "string" }).array(); +const TarballPutShape = type({ commit: "string", integrity: "string" }); + +/** Idempotently ensures the tenant's `corbits-tools` package-registry + * asset exists, mirroring `myra-deploy.ts`'s `ensureMyraSourceAsset`. */ +export async function ensureCorbitsToolsRegistryAsset( + tenantId: string, + fetchImpl: typeof fetch = fetch, +): Promise { + const created = await fetchImpl(`/api/tenants/${encodeURIComponent(tenantId)}/assets`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ kind: "package-registry", name: CORBITS_TOOLS_REGISTRY }), + }); + if (created.status === 201) { + const parsed = AssetCreatedShape(await created.json()); + if (parsed instanceof type.errors) { + throw new RegistryPublishError( + `the ${CORBITS_TOOLS_REGISTRY} registry came back an unexpected shape: ${parsed.summary}`, + ); + } + return parsed.id; + } + if (created.status !== 409) { + throw new RegistryPublishError( + `preparing the ${CORBITS_TOOLS_REGISTRY} registry failed: ${await readErrorBody(created)}`, + ); + } + const listed = await fetchImpl( + `/api/tenants/${encodeURIComponent(tenantId)}/assets?kind=package-registry&inherited=false`, + ); + if (!listed.ok) { + throw new RegistryPublishError( + `checking the ${CORBITS_TOOLS_REGISTRY} registry failed: ${await readErrorBody(listed)}`, + ); + } + const parsed = AssetListShape(await listed.json()); + if (parsed instanceof type.errors) { + throw new RegistryPublishError( + `the ${CORBITS_TOOLS_REGISTRY} registry's asset list came back an unexpected shape: ${parsed.summary}`, + ); + } + const existing = parsed.find((asset) => asset.name === CORBITS_TOOLS_REGISTRY); + if (existing === undefined) { + throw new RegistryPublishError( + `the ${CORBITS_TOOLS_REGISTRY} registry reported a name conflict but is not listed on this workbench`, + ); + } + return existing.id; +} + +// Specifiers that only resolve inside this monorepo's own workspace — +// resolved to the concrete version a published tarball's consumer can +// actually install, taken from the root `catalog` this repo pins them at. +const CATALOG_VERSIONS: Record = { + arktype: "^2.2.0", + semver: "^7.7.2", +}; + +function resolvedDependencySpec(packageName: string, spec: string): string { + if (spec === "catalog:") { + const resolved = CATALOG_VERSIONS[packageName]; + if (resolved === undefined) { + throw new RegistryPublishError( + `registry-publish: no catalog version recorded for "${packageName}" — add it to CATALOG_VERSIONS`, + ); + } + return resolved; + } + return spec; +} + +type ToolPackageManifest = { + readonly name: string; + readonly version: string; + readonly exports?: Record; + readonly dependencies?: Record; + readonly peerDependencies?: Record; +}; + +export type ToolPackageTarball = { readonly name: string; readonly version: string }; + +function tarballFilenameFor(name: string, version: string): string { + return `${name.replace(/^@/, "").replace("/", "-")}-${version}.tgz`; +} + +const TOOL_PACKAGE_MANIFESTS = import.meta.glob("../../../../tools/*/package.json", { + query: "?raw", + import: "default", + eager: true, +}) as Record; + +const TOOL_PACKAGE_SOURCE_FILES = import.meta.glob("../../../../tools/*/src/**/*.ts", { + query: "?raw", + import: "default", + eager: true, +}) as Record; + +function toolDirName(manifestPath: string): string { + const match = /\/tools\/([^/]+)\/package\.json$/.exec(manifestPath); + const dir = match?.[1]; + if (dir === undefined) { + throw new RegistryPublishError(`registry-publish: unexpected manifest path ${manifestPath}`); + } + return dir; +} + +/** One `tools/*` package's manifest plus every `src/**\/*.ts` file it + * ships, staged the same shape `packTarball` expects. */ +function buildToolPackageTree( + manifestPath: string, + manifestJson: string, +): { + name: string; + version: string; + tree: Record; +} { + const manifest = JSON.parse(manifestJson) as ToolPackageManifest; + const dependencies = Object.fromEntries( + Object.entries({ ...manifest.dependencies, ...manifest.peerDependencies }).map( + ([name, spec]) => [name, resolvedDependencySpec(name, spec)], + ), + ); + const dir = toolDirName(manifestPath); + const prefix = `../../../../tools/${dir}/`; + const tree: Record = { + "package.json": JSON.stringify( + { + name: manifest.name, + version: manifest.version, + type: "module", + exports: manifest.exports, + dependencies, + interchange: { tools: manifest.exports?.["."] ?? "./src/index.ts" }, + }, + null, + 2, + ), + }; + for (const [sourcePath, contents] of Object.entries(TOOL_PACKAGE_SOURCE_FILES)) { + if (!sourcePath.startsWith(prefix)) continue; + tree[sourcePath.slice(prefix.length)] = contents; + } + return { name: manifest.name, version: manifest.version, tree }; +} + +/** Every `tools/*` package this workspace ships, as the `packTarball`-ready + * trees `publishToolPackageRegistry` packs and uploads. Exported standalone + * so the packaging is unit-testable without a fetch round trip. */ +export function buildToolPackageTrees(): { + name: string; + version: string; + tree: Record; +}[] { + return Object.entries(TOOL_PACKAGE_MANIFESTS).map(([manifestPath, manifestJson]) => + buildToolPackageTree(manifestPath, manifestJson), + ); +} + +/** Packs and publishes every `tools/*` package into the tenant's + * `corbits-tools` registry, ensuring the registry asset first. Returns + * the filenames it published. */ +export async function publishToolPackageRegistry( + tenantId: string, + fetchImpl: typeof fetch = fetch, +): Promise { + const assetId = await ensureCorbitsToolsRegistryAsset(tenantId, fetchImpl); + const published: ToolPackageTarball[] = []; + for (const { name, version, tree } of buildToolPackageTrees()) { + const tarball = await packTarball(tree); + const filename = tarballFilenameFor(name, version); + const response = await fetchImpl( + `/api/tenants/${encodeURIComponent(tenantId)}/assets/${encodeURIComponent(assetId)}/tarballs/${encodeURIComponent(filename)}`, + { + method: "PUT", + headers: { "content-type": "application/octet-stream" }, + body: new Blob([tarball as Uint8Array]), + }, + ); + if (!response.ok) { + throw new RegistryPublishError(`publishing ${name} failed: ${await readErrorBody(response)}`); + } + const parsed = TarballPutShape(await response.json()); + if (parsed instanceof type.errors) { + throw new RegistryPublishError( + `${name}'s tarball upload came back an unexpected shape: ${parsed.summary}`, + ); + } + published.push({ name, version }); + } + return published; +} diff --git a/apps/web/test/library/artifact-card.test.tsx b/apps/web/test/library/artifact-card.test.tsx index 78b2d899e5..6d2ab33fb7 100644 --- a/apps/web/test/library/artifact-card.test.tsx +++ b/apps/web/test/library/artifact-card.test.tsx @@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; import { ArtifactCard } from "../../src/library/artifact-card"; -import type { ArtifactSummary } from "@corbits/artifact-ui/types"; +import type { ArtifactSummary } from "../../src/library/artifact-summary"; function artifact(overrides: Partial & { readonly id: string }): ArtifactSummary { return { diff --git a/bun.lock b/bun.lock index bb8d149729..2a531291d8 100644 --- a/bun.lock +++ b/bun.lock @@ -54,7 +54,6 @@ "@chat-adapter/state-memory": "^4.35.0", "@corbits/agent-directory": "workspace:*", "@corbits/approvals": "workspace:*", - "@corbits/artifact-ui": "workspace:*", "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#5309a15d4aa504f132c54b4da08bb672b4903873", "@corbits/codex-provider": "github:corbitsdev/corbits-codex-provider#a1f475f341261c05fa8dade0c82f1246c175b5b3", "@corbits/cron": "workspace:*", @@ -62,7 +61,6 @@ "@corbits/inference-catalog": "workspace:*", "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38", "@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913", - "@corbits/tool-registry-publish": "workspace:*", "@corbits/url-path": "workspace:*", "@corbits/webhook-triggers": "workspace:*", "@corbits/workflows": "workspace:*", @@ -133,7 +131,6 @@ "dependencies": { "@corbits/agent-directory": "workspace:*", "@corbits/approvals": "workspace:*", - "@corbits/artifact-ui": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/myra": "workspace:*", "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", @@ -166,10 +163,8 @@ "name": "@corbits/agent-directory", "version": "0.0.1", "dependencies": { - "@corbits/agent-events": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/skills-tools": "workspace:*", - "@corbits/tool-registry-publish": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", @@ -194,14 +189,6 @@ "typescript": "catalog:", }, }, - "packages/agent-events": { - "name": "@corbits/agent-events", - "version": "0.0.1", - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, "packages/approvals": { "name": "@corbits/approvals", "version": "0.0.1", @@ -221,18 +208,6 @@ "typescript": "catalog:", }, }, - "packages/artifact-ui": { - "name": "@corbits/artifact-ui", - "version": "0.0.1", - "dependencies": { - "@corbits/url-path": "workspace:*", - "arktype": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, "packages/collections": { "name": "@corbits/collections", "version": "0.0.1", @@ -287,21 +262,6 @@ "@intx/types": "0.3.0", }, }, - "packages/tool-registry-publish": { - "name": "@corbits/tool-registry-publish", - "version": "0.0.1", - "dependencies": { - "@intx/inference": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "tar": "catalog:", - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", - }, - }, "packages/url-path": { "name": "@corbits/url-path", "version": "0.0.1", @@ -365,7 +325,6 @@ "devDependencies": { "@corbits/agent-directory": "workspace:*", "@corbits/agent-directory-tools": "workspace:*", - "@corbits/tool-registry-publish": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:", }, @@ -848,12 +807,8 @@ "@corbits/agent-directory-tools": ["@corbits/agent-directory-tools@workspace:tools/agent-directory"], - "@corbits/agent-events": ["@corbits/agent-events@workspace:packages/agent-events"], - "@corbits/approvals": ["@corbits/approvals@workspace:packages/approvals"], - "@corbits/artifact-ui": ["@corbits/artifact-ui@workspace:packages/artifact-ui"], - "@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#5309a15", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "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-5309a15", "sha512-zGHkq++juclUrOd0O8/Z5ziTldmJZ6gqyH/c9cs5S10UPU4gqn8CHwq174SiJXY0ujJ3cNpJXnrr+0fcqlfQ1w=="], "@corbits/capability-tools": ["@corbits/capability-tools@workspace:tools/capability"], @@ -892,8 +847,6 @@ "@corbits/skills-tools": ["@corbits/skills-tools@workspace:tools/skills"], - "@corbits/tool-registry-publish": ["@corbits/tool-registry-publish@workspace:packages/tool-registry-publish"], - "@corbits/url-path": ["@corbits/url-path@workspace:packages/url-path"], "@corbits/webhook-triggers": ["@corbits/webhook-triggers@workspace:packages/webhook-triggers"], @@ -2062,6 +2015,8 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@corbits/inference-catalog/@corbits/ollama-adapter": ["@corbits/ollama-adapter@github:corbitsdev/corbits-ollama-adapter#0387eb1", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-ollama-adapter-0387eb1", "sha512-O1dl5lSwWzNGJibQLlpYDFKJZWpstgHX519bl8oe1ALL/KHszT/tBDlpsD9jN3hmQD3jV2L1jNwvOwLOBLtpyw=="], + "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -2080,8 +2035,16 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#5309a15", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "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-5309a15", "sha512-zGHkq++juclUrOd0O8/Z5ziTldmJZ6gqyH/c9cs5S10UPU4gqn8CHwq174SiJXY0ujJ3cNpJXnrr+0fcqlfQ1w=="], + + "@workbench/hub/@corbits/codex-provider": ["@corbits/codex-provider@github:corbitsdev/corbits-codex-provider#a1f475f", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-codex-provider-a1f475f", "sha512-D6d0lblWx/4KudfLwQoQGrLB8GKRLP/HVp1cx08L2/hOGZAgH6txLPpeEtBqjl+R520qXSSPfM0kDC+3XNaAfw=="], + + "@workbench/hub/@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#e85b81e", { "dependencies": { "@hono/standard-validator": "0.2.3", "@intx/crypto": "0.3.0", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-e85b81e", "sha512-ATPP/pll1sYG1DLKV1HvtboK8q1tFkqayQqRXdUmRUIsFJx5wqzd8YA1XELjXzLu1P2f2uCMe2CjsJHGv6E90Q=="], + "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#e74da20", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-e74da20", "sha512-/uA9xTcF7XwMXgnR8OZamierzkLG4g4mhw3u9Fl3Uj6ait8pH3wcoQ6V32on1nMdaqtPRdYVFnxof5O+4ZacGA=="], + "@workbench/hub/@corbits/xai-provider": ["@corbits/xai-provider@github:corbitsdev/corbits-xai-provider#9f2d4ba", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-xai-provider-9f2d4ba", "sha512-ZjvKxGcp+QzCh8zeXHg7kNbtkpR/itZkmQAcdG1aOAOjHaNwfGVpkPc/c/XtwPNsQyaDjF8W9CHb4rH1xI2uCA=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], @@ -2172,6 +2135,16 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@workbench/hub/@corbits/codex-provider/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#1b7f9fb", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-1b7f9fb", "sha512-PscyiDAl46umTM44NQPN1q5+PsGUV53Rxr4Wz/Ofz+k9LOOz6FBKamRFtWR8YWdKJt9I8sPOLOyEcMNNN2i+YQ=="], + + "@workbench/hub/@corbits/codex-provider/@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#7efe000", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-7efe000", "sha512-pH6C8T1WDJjqM+YbnQpofd7HgP5krEj48JCCQK16KI98a6kaz+gffpgdOdQpU3aNVZnFlyADXod1Z4tqUG/3ww=="], + + "@workbench/hub/@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], + + "@workbench/hub/@corbits/xai-provider/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#1b7f9fb", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-1b7f9fb", "sha512-PscyiDAl46umTM44NQPN1q5+PsGUV53Rxr4Wz/Ofz+k9LOOz6FBKamRFtWR8YWdKJt9I8sPOLOyEcMNNN2i+YQ=="], + + "@workbench/hub/@corbits/xai-provider/@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#7efe000", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-7efe000", "sha512-pH6C8T1WDJjqM+YbnQpofd7HgP5krEj48JCCQK16KI98a6kaz+gffpgdOdQpU3aNVZnFlyADXod1Z4tqUG/3ww=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], diff --git a/packages/agent-directory/package.json b/packages/agent-directory/package.json index c2f21865b7..f87edf198f 100644 --- a/packages/agent-directory/package.json +++ b/packages/agent-directory/package.json @@ -14,10 +14,8 @@ "test": "bun test" }, "dependencies": { - "@corbits/agent-events": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/skills-tools": "workspace:*", - "@corbits/tool-registry-publish": "workspace:*", "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", diff --git a/packages/agent-directory/src/agent-events.test.ts b/packages/agent-directory/src/agent-events.test.ts new file mode 100644 index 0000000000..13fa35763b --- /dev/null +++ b/packages/agent-directory/src/agent-events.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from "bun:test"; + +import { connectorReplyContent, messageRunEnded } from "./agent-events"; + +describe("connectorReplyContent", () => { + test("reads the content off a connector.reply", () => { + expect( + connectorReplyContent({ + type: "connector.reply", + seq: 1, + data: { content: "All clear." }, + }), + ).toBe("All clear."); + }); + + test("an empty reply reads as no reply", () => { + expect( + connectorReplyContent({ + type: "connector.reply", + seq: 1, + data: { content: "" }, + }), + ).toBeUndefined(); + }); + + test("any other event reads as no reply", () => { + expect(connectorReplyContent({ type: "reactor.done" })).toBeUndefined(); + expect(connectorReplyContent(null)).toBeUndefined(); + expect(connectorReplyContent("connector.reply")).toBeUndefined(); + }); +}); + +describe("messageRunEnded", () => { + test("reads a completed bracket close", () => { + expect( + messageRunEnded({ + type: "message.run.ended", + seq: 2, + data: { status: "completed" }, + }), + ).toEqual({ status: "completed", errorMessage: undefined }); + }); + + test("reads a failed bracket close with its error message", () => { + expect( + messageRunEnded({ + type: "message.run.ended", + seq: 2, + data: { status: "failed", error: { message: "tool exploded" } }, + }), + ).toEqual({ status: "failed", errorMessage: "tool exploded" }); + }); + + test("an unknown status reads as no terminal signal", () => { + expect( + messageRunEnded({ + type: "message.run.ended", + seq: 2, + data: { status: "cancelled" }, + }), + ).toBeUndefined(); + }); + + test("any other event reads as no terminal signal", () => { + expect(messageRunEnded({ type: "connector.reply" })).toBeUndefined(); + expect(messageRunEnded(undefined)).toBeUndefined(); + }); +}); diff --git a/packages/agent-directory/src/agent-events.ts b/packages/agent-directory/src/agent-events.ts new file mode 100644 index 0000000000..59162fac92 --- /dev/null +++ b/packages/agent-directory/src/agent-events.ts @@ -0,0 +1,41 @@ +// Recognizers for the sidecar `agent.event` frames `one-shot-prompt` keys +// off. These readers stay structural since the stream's payload arrives +// as `unknown`. + +/** The reply text of a `connector.reply` event, or undefined for any + * other event or an empty reply. */ +export function connectorReplyContent(event: unknown): string | undefined { + if ( + typeof event !== "object" || + event === null || + (event as { type?: unknown }).type !== "connector.reply" + ) { + return undefined; + } + const content = (event as { data?: { content?: unknown } }).data?.content; + return typeof content === "string" && content !== "" ? content : undefined; +} + +export type MessageRunEnded = { + readonly status: "completed" | "failed"; + readonly errorMessage: string | undefined; +}; + +/** A `message.run.ended` bracket close — the harness's own per-message + * terminal signal (`status: "completed" | "failed"`) — or undefined for + * any other event. */ +export function messageRunEnded(event: unknown): MessageRunEnded | undefined { + if ( + typeof event !== "object" || + event === null || + (event as { type?: unknown }).type !== "message.run.ended" + ) { + return undefined; + } + const data = (event as { data?: { status?: unknown; error?: { message?: unknown } } }).data; + if (data?.status !== "completed" && data?.status !== "failed") { + return undefined; + } + const errorMessage = typeof data.error?.message === "string" ? data.error.message : undefined; + return { status: data.status, errorMessage }; +} diff --git a/packages/agent-directory/src/agent-workflow.test.ts b/packages/agent-directory/src/agent-workflow.test.ts index 0749190d2f..550284a134 100644 --- a/packages/agent-directory/src/agent-workflow.test.ts +++ b/packages/agent-directory/src/agent-workflow.test.ts @@ -1,12 +1,8 @@ -// `SKILLS_TOOL_PACKAGE_PIN` named -// `@corbits/skills-tools`, but `tool-registry-publish`'s -// `CORBITS_TOOL_PACKAGE_DIRS` never listed that package's directory, so -// a definition pinning skills carried a pin the corbits-tools registry -// could never resolve at launch. import { describe, expect, test } from "bun:test"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; import type { DB } from "@intx/db"; import type { AssetService } from "@intx/hub-sessions"; -import { CORBITS_TOOL_PACKAGE_DIRS, packToolPackageTarball } from "@corbits/tool-registry-publish"; import { buildAgentDefinitionWorkflow, createAgentDefinitionCore, @@ -18,16 +14,17 @@ import { } from "./agent-workflow"; describe("SKILLS_TOOL_PACKAGE_PIN", () => { - test("resolves through the corbits-tools registry", async () => { - // Assert against what the registry actually carries: the packed - // tarball for the pinned package, not a source-tree import. A - // package's directory name need not match its `@corbits/*` name - // (e.g. `tools/skills` publishes `@corbits/skills-tools`), so this - // packs every registered directory rather than guessing one by path. - const tarballs = await Promise.all(CORBITS_TOOL_PACKAGE_DIRS.map(packToolPackageTarball)); - const tarball = tarballs.find((candidate) => candidate.name === SKILLS_TOOL_PACKAGE_PIN.name); - expect(tarball).toBeDefined(); - expect(tarball?.version).toBe(SKILLS_TOOL_PACKAGE_PIN.version); + test("matches tools/skills's own published name and version", async () => { + // A hardcoded pin drifts silently from its source package's version; + // this catches that the moment `tools/skills`'s package.json bumps + // without the pin following. + const manifestPath = path.resolve(import.meta.dir, "../../../tools/skills/package.json"); + const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as { + name: string; + version: string; + }; + expect(manifest.name).toBe(SKILLS_TOOL_PACKAGE_PIN.name); + expect(manifest.version).toBe(SKILLS_TOOL_PACKAGE_PIN.version); }); }); diff --git a/packages/agent-directory/src/one-shot-prompt.ts b/packages/agent-directory/src/one-shot-prompt.ts index ac91c2c684..670cbdab09 100644 --- a/packages/agent-directory/src/one-shot-prompt.ts +++ b/packages/agent-directory/src/one-shot-prompt.ts @@ -4,7 +4,7 @@ // soon as the run starts; this module turns the event stream into an // awaitable promise for a caller that has no later-delivery surface. import { and, eq } from "drizzle-orm"; -import { connectorReplyContent, messageRunEnded } from "@corbits/agent-events"; +import { connectorReplyContent, messageRunEnded } from "./agent-events"; import { reportError } from "@corbits/error-sink"; import { deliverWhenRoutable, diff --git a/packages/agent-directory/src/tool-package-version.test.ts b/packages/agent-directory/src/tool-package-version.test.ts index 56d3db24ad..f83d347d89 100644 --- a/packages/agent-directory/src/tool-package-version.test.ts +++ b/packages/agent-directory/src/tool-package-version.test.ts @@ -9,7 +9,7 @@ import { expect, test } from "bun:test"; import type { DB } from "@intx/db"; import type { AssetService } from "@intx/hub-sessions"; -import { CORBITS_TOOLS_REGISTRY } from "@corbits/tool-registry-publish"; +import { CORBITS_TOOLS_REGISTRY } from "./tool-registry"; import { CapabilityOutOfInventoryError } from "./capability-inventory"; import { createPinnedVersionResolver, resolvePinnedVersion } from "./tool-package-version"; diff --git a/packages/agent-directory/src/tool-package-version.ts b/packages/agent-directory/src/tool-package-version.ts index 0da3d7d177..c94f8f598c 100644 --- a/packages/agent-directory/src/tool-package-version.ts +++ b/packages/agent-directory/src/tool-package-version.ts @@ -14,15 +14,16 @@ // (`vendor/intx/hub-sessions/src/session-service.ts`'s `buildAndResolve`). // Reading the tarball listing directly here, rather than re-deriving a // packument, keeps this resolver a plain filename scan: exactly the -// same `tarballCoversPackage` filter `@corbits/tool-registry-publish` -// already uses to decide whether a registry carries a given package. +// same `tarballCoversPackage` filter the web app's own registry publisher +// (`apps/web/src/tools/registry-publish.ts`) uses to decide whether a +// registry carries a given package. import semver from "semver"; import type { DB } from "@intx/db"; import { resolveAssetByName } from "@intx/db"; import type { AssetService } from "@intx/hub-sessions"; -import { CORBITS_TOOLS_REGISTRY, tarballCoversPackage } from "@corbits/tool-registry-publish"; +import { CORBITS_TOOLS_REGISTRY, tarballCoversPackage } from "./tool-registry"; import { CapabilityOutOfInventoryError } from "./capability-inventory"; @@ -40,9 +41,8 @@ export type ResolvePinnedVersionDeps = { readonly assetService: AssetService; }; -/** The `-.tgz` filename convention - * `tarballFilenameFor`/`packToolPackageTarball` (`@corbits/tool-registry-publish`'s - * `pack.ts`) write into a registry's `tarballs/` tree. Returns `null` +/** The `-.tgz` filename convention the web app's registry + * publisher writes into a registry's `tarballs/` tree. Returns `null` * for a filename that does not cover `packageName` or does not parse as * a valid semver version — a defensively-shaped filename is treated as * absent rather than crashing the resolution. */ diff --git a/packages/agent-directory/src/tool-registry.ts b/packages/agent-directory/src/tool-registry.ts new file mode 100644 index 0000000000..3f2502fcf9 --- /dev/null +++ b/packages/agent-directory/src/tool-registry.ts @@ -0,0 +1,17 @@ +// The one site naming the tenant-scoped package-registry asset that +// resolves `@corbits/*` tool-package pins, and the filename filter that +// decides whether a registry's tarball listing carries a given package. +// A rename changes one file instead of chasing string literals across +// `apps/hub` (scope routing) and every publish call site. + +/** + * Asset name the hub's `@corbits` scope routing resolves tool-package + * pins against (`apps/hub/src/index.ts`'s `CORBITS_TOOLS_REGISTRY`). + */ +export const CORBITS_TOOLS_REGISTRY = "corbits-tools"; + +/** True when `filename` is an npm-style tarball for `packageName` (any version). */ +export function tarballCoversPackage(filename: string, packageName: string): boolean { + const prefix = `${packageName.replace(/^@/, "").replace("/", "-")}-`; + return filename.startsWith(prefix) && filename.endsWith(".tgz"); +} diff --git a/packages/agent-directory/test/routes.test.ts b/packages/agent-directory/test/routes.test.ts index 75a4dc7ded..2178809fd2 100644 --- a/packages/agent-directory/test/routes.test.ts +++ b/packages/agent-directory/test/routes.test.ts @@ -15,7 +15,7 @@ import { AssetServiceError } from "@intx/hub-sessions"; import type { AssetService } from "@intx/hub-sessions"; import type { DB } from "@intx/db"; -import { CORBITS_TOOLS_REGISTRY } from "@corbits/tool-registry-publish"; +import { CORBITS_TOOLS_REGISTRY } from "../src/tool-registry"; import { buildAgentDefinitionWorkflow, diff --git a/packages/agent-directory/test/tool-package-version.drizzle.test.ts b/packages/agent-directory/test/tool-package-version.drizzle.test.ts index 2411264257..05e8864ac8 100644 --- a/packages/agent-directory/test/tool-package-version.drizzle.test.ts +++ b/packages/agent-directory/test/tool-package-version.drizzle.test.ts @@ -15,7 +15,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import { randomUUID } from "node:crypto"; import { createDB, dropSchema, runMigrations, schema } from "@intx/db"; import type { AssetService } from "@intx/hub-sessions"; -import { CORBITS_TOOLS_REGISTRY } from "@corbits/tool-registry-publish"; +import { CORBITS_TOOLS_REGISTRY } from "../src/tool-registry"; import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { e2eDatabaseUrl } from "../../../test/database-url"; diff --git a/packages/agent-directory/test/workflow-capability-routes.test.ts b/packages/agent-directory/test/workflow-capability-routes.test.ts index 04c5eeb3a7..3ad9be68e9 100644 --- a/packages/agent-directory/test/workflow-capability-routes.test.ts +++ b/packages/agent-directory/test/workflow-capability-routes.test.ts @@ -25,7 +25,7 @@ import { agentDefinitionSourceTree, AGENT_DEFINITION_ENTRY_PATH } from "../src/d import type { PinnedSkillIndexResolver } from "../src/routes"; import { definitionFrom, SOURCE_TREE_PATHS } from "./source-tree"; import type { CapabilityInventoryProvider } from "../src/capability-inventory"; -import { CORBITS_TOOLS_REGISTRY } from "@corbits/tool-registry-publish"; +import { CORBITS_TOOLS_REGISTRY } from "../src/tool-registry"; const TENANT_ID = "tnt_1"; const RUN_ID = "run_1"; diff --git a/packages/agent-directory/test/workflow-create-routes.test.ts b/packages/agent-directory/test/workflow-create-routes.test.ts index cabb98993d..e555747b74 100644 --- a/packages/agent-directory/test/workflow-create-routes.test.ts +++ b/packages/agent-directory/test/workflow-create-routes.test.ts @@ -20,7 +20,7 @@ import type { } from "../src/workflow-capability-routes"; import type { PinnedSkillIndexResolver } from "../src/routes"; import type { CapabilityInventoryProvider } from "../src/capability-inventory"; -import { CORBITS_TOOLS_REGISTRY } from "@corbits/tool-registry-publish"; +import { CORBITS_TOOLS_REGISTRY } from "../src/tool-registry"; import { definitionFrom, SOURCE_TREE_PATHS } from "./source-tree"; const TENANT_ID = "tnt_1"; diff --git a/packages/agent-events/LICENSE b/packages/agent-events/LICENSE deleted file mode 100644 index 173c3a21d0..0000000000 --- a/packages/agent-events/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! diff --git a/packages/agent-events/package.json b/packages/agent-events/package.json deleted file mode 100644 index 03abbceabe..0000000000 --- a/packages/agent-events/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@corbits/agent-events", - "version": "0.0.1", - "private": true, - "description": "Structural readers for the sidecar agent.event frames: connector replies, inference-done content blocks, tool-done results, and message-run start/end brackets", - "license": "LGPL-2.1-only", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - } -} diff --git a/packages/agent-events/src/agent-events.test.ts b/packages/agent-events/src/agent-events.test.ts deleted file mode 100644 index fbf2f190b3..0000000000 --- a/packages/agent-events/src/agent-events.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { - connectorReplyContent, - inferenceDoneBlocks, - messageRunBracket, - messageRunEnded, - toolDoneResult, -} from "./agent-events"; - -describe("connectorReplyContent", () => { - test("reads the content off a connector.reply", () => { - expect( - connectorReplyContent({ - type: "connector.reply", - seq: 1, - data: { content: "All clear." }, - }), - ).toBe("All clear."); - }); - - test("an empty reply reads as no reply", () => { - expect( - connectorReplyContent({ - type: "connector.reply", - seq: 1, - data: { content: "" }, - }), - ).toBeUndefined(); - }); - - test("any other event reads as no reply", () => { - expect(connectorReplyContent({ type: "reactor.done" })).toBeUndefined(); - expect(connectorReplyContent(null)).toBeUndefined(); - expect(connectorReplyContent("connector.reply")).toBeUndefined(); - }); -}); - -describe("inferenceDoneBlocks", () => { - test("splits an inference.done turn into text and tool-call blocks, in order", () => { - expect( - inferenceDoneBlocks({ - type: "inference.done", - seq: 1, - data: { - turn: { - content: [ - { type: "text", text: "Let me check that." }, - { - type: "tool_call", - id: "call_1", - name: "web_search", - arguments: { query: "web search browser" }, - }, - { type: "text", text: "Here's what I found." }, - ], - }, - }, - }), - ).toEqual([ - { kind: "text", text: "Let me check that." }, - { - kind: "tool-call", - callId: "call_1", - name: "web_search", - input: { query: "web search browser" }, - }, - { kind: "text", text: "Here's what I found." }, - ]); - }); - - test("drops block kinds with no chat-part equivalent (thinking, citations, ...)", () => { - expect( - inferenceDoneBlocks({ - type: "inference.done", - seq: 1, - data: { - turn: { - content: [ - { type: "thinking", thinking: "internal reasoning" }, - { type: "text", text: "Visible reply." }, - ], - }, - }, - }), - ).toEqual([{ kind: "text", text: "Visible reply." }]); - }); - - test("drops empty text blocks", () => { - expect( - inferenceDoneBlocks({ - type: "inference.done", - seq: 1, - data: { turn: { content: [{ type: "text", text: "" }] } }, - }), - ).toEqual([]); - }); - - test("any other event reads as no blocks", () => { - expect(inferenceDoneBlocks({ type: "connector.reply" })).toBeUndefined(); - expect(inferenceDoneBlocks(null)).toBeUndefined(); - }); -}); - -describe("toolDoneResult", () => { - test("reads a successful tool result", () => { - expect( - toolDoneResult({ - type: "tool.done", - seq: 1, - data: { result: { callId: "call_1", content: "3 results found" } }, - }), - ).toEqual({ callId: "call_1", content: "3 results found", isError: false }); - }); - - test("reads a failed tool result", () => { - expect( - toolDoneResult({ - type: "tool.done", - seq: 1, - data: { - result: { callId: "call_1", content: "timed out", isError: true }, - }, - }), - ).toEqual({ callId: "call_1", content: "timed out", isError: true }); - }); - - test("any other event reads as no result", () => { - expect(toolDoneResult({ type: "connector.reply" })).toBeUndefined(); - expect(toolDoneResult(undefined)).toBeUndefined(); - }); - - test("carries a failed tool's structured detail alongside its content", () => { - expect( - toolDoneResult({ - type: "tool.done", - seq: 1, - data: { - result: { - callId: "call_1", - content: "GitHub is not connected.", - isError: true, - detail: { kind: "missing-credential", connectorId: "github" }, - }, - }, - }), - ).toEqual({ - callId: "call_1", - content: "GitHub is not connected.", - isError: true, - detail: { kind: "missing-credential", connectorId: "github" }, - }); - }); -}); - -describe("messageRunEnded", () => { - test("reads a completed bracket close", () => { - expect( - messageRunEnded({ - type: "message.run.ended", - seq: 2, - data: { status: "completed" }, - }), - ).toEqual({ status: "completed", errorMessage: undefined }); - }); - - test("reads a failed bracket close with its error message", () => { - expect( - messageRunEnded({ - type: "message.run.ended", - seq: 2, - data: { status: "failed", error: { message: "tool exploded" } }, - }), - ).toEqual({ status: "failed", errorMessage: "tool exploded" }); - }); - - test("an unknown status reads as no terminal signal", () => { - expect( - messageRunEnded({ - type: "message.run.ended", - seq: 2, - data: { status: "cancelled" }, - }), - ).toBeUndefined(); - }); - - test("any other event reads as no terminal signal", () => { - expect(messageRunEnded({ type: "connector.reply" })).toBeUndefined(); - expect(messageRunEnded(undefined)).toBeUndefined(); - }); -}); - -describe("messageRunBracket", () => { - test("reads the identity off a bracket open", () => { - expect( - messageRunBracket({ - type: "message.run.started", - seq: 1, - data: { - messageId: "", - messageRunId: "run_1", - receivedAt: 123, - }, - }), - ).toEqual({ - messageId: "", - messageRunId: "run_1", - }); - }); - - test("reads the identity off a bracket close", () => { - expect( - messageRunBracket({ - type: "message.run.ended", - seq: 2, - data: { - messageRunId: "run_1", - messageId: "", - status: "completed", - }, - }), - ).toEqual({ - messageId: "", - messageRunId: "run_1", - }); - }); - - test("any other event, or a bracket missing its ids, reads as no bracket", () => { - expect(messageRunBracket({ type: "connector.reply" })).toBeUndefined(); - expect(messageRunBracket(undefined)).toBeUndefined(); - expect(messageRunBracket({ type: "message.run.started", data: {} })).toBeUndefined(); - expect( - messageRunBracket({ - type: "message.run.ended", - data: { messageId: "" }, - }), - ).toBeUndefined(); - }); -}); diff --git a/packages/agent-events/src/agent-events.ts b/packages/agent-events/src/agent-events.ts deleted file mode 100644 index 78bdc76b18..0000000000 --- a/packages/agent-events/src/agent-events.ts +++ /dev/null @@ -1,178 +0,0 @@ -// Recognizers for the sidecar `agent.event` frames every folded-run -// observer keys off. `@corbits/chat`'s own process-wide orchestrator -// (replies into workbenches) subscribes to this same stream and needs -// these same two readings, so the parsing lives here, in a package it -// already builds on rather than duplicated inline. This module depends -// on nothing but -// the event shapes (`@intx/types`' `AgentEvent` union documents them; -// these readers stay structural since the stream's payload arrives as -// `unknown`), which is what keeps it importable from a browser context -// too (e.g. a future `@corbits/chat-ui` consumer). - -/** The reply text of a `connector.reply` event, or undefined for any - * other event or an empty reply. */ -export function connectorReplyContent(event: unknown): string | undefined { - if ( - typeof event !== "object" || - event === null || - (event as { type?: unknown }).type !== "connector.reply" - ) { - return undefined; - } - const content = (event as { data?: { content?: unknown } }).data?.content; - return typeof content === "string" && content !== "" ? content : undefined; -} - -/** One block of a `connector.reply`-producing turn, narrowed to the two - * kinds a reply's chat representation must tell apart: visible - * text, and a tool the model invoked. Every other `ContentBlock` variant - * (thinking, citations, safety ratings, ...) carries no chat-part - * equivalent yet and is left out rather than guessed at. */ -export type ReplyContentBlock = - | { readonly kind: "text"; readonly text: string } - | { - readonly kind: "tool-call"; - readonly callId: string; - readonly name: string; - readonly input: Record; - }; - -/** The ordered content blocks of an `inference.done` turn, narrowed to - * `ReplyContentBlock`s, or undefined for any other event. This is the - * structured alternative to `connectorReplyContent`'s flattened string: - * reading blocks straight off `inference.done` (where the harness has - * already separated a model's prose from its tool calls, see - * `vendor/intx/hub-sessions/src/event-collector.ts`'s `handleInferenceDone`) - * means a chat orchestrator building message parts never has to guess - * which part of a reply's text was actually tool-call JSON the model - * emitted inline — it reads the split the harness already made instead of - * re-deriving it from prose. */ -export function inferenceDoneBlocks(event: unknown): ReplyContentBlock[] | undefined { - if ( - typeof event !== "object" || - event === null || - (event as { type?: unknown }).type !== "inference.done" - ) { - return undefined; - } - const content = (event as { data?: { turn?: { content?: unknown } } }).data?.turn?.content; - if (!Array.isArray(content)) return undefined; - - const blocks: ReplyContentBlock[] = []; - for (const block of content as unknown[]) { - if (typeof block !== "object" || block === null) continue; - const typed = block as { type?: unknown }; - if (typed.type === "text") { - const text = (block as { text?: unknown }).text; - if (typeof text === "string" && text !== "") { - blocks.push({ kind: "text", text }); - } - } else if (typed.type === "tool_call") { - const id = (block as { id?: unknown }).id; - const name = (block as { name?: unknown }).name; - const args = (block as { arguments?: unknown }).arguments; - if ( - typeof id === "string" && - typeof name === "string" && - typeof args === "object" && - args !== null - ) { - blocks.push({ - kind: "tool-call", - callId: id, - name, - input: args as Record, - }); - } - } - } - return blocks; -} - -/** A `tool.done` result, keyed by the `callId` it resolves — or undefined - * for any other event. Pairs with `inferenceDoneBlocks`' `tool-call` - * entries to fill in a tool-trace part's outcome once its call settles. */ -export function toolDoneResult( - event: unknown, -): { callId: string; content: unknown; isError: boolean; detail?: unknown } | undefined { - if ( - typeof event !== "object" || - event === null || - (event as { type?: unknown }).type !== "tool.done" - ) { - return undefined; - } - const result = (event as { data?: { result?: unknown } }).data?.result; - if (typeof result !== "object" || result === null) return undefined; - const callId = (result as { callId?: unknown }).callId; - if (typeof callId !== "string") return undefined; - const content = (result as { content?: unknown }).content; - const isError = (result as { isError?: unknown }).isError === true; - // `detail` rides alongside `content` on the underlying `ToolResult` - // (`@intx/types/runtime`) as the side channel for structured metadata - // that isn't meant for the model's own eyes. - const detail = (result as { detail?: unknown }).detail; - return { callId, content, isError, detail }; -} - -export type MessageRunEnded = { - readonly status: "completed" | "failed"; - readonly errorMessage: string | undefined; -}; - -/** The identity half of a `message.run.started`/`message.run.ended` - * bracket pair — the two ids every open and close both carry. Chat's - * orchestrator pairs them by `messageRunId` (reactor-minted per dequeue, - * so a crash-and-replay of the same `messageId` still produces - * unambiguous pairs) and matches `messageId` back to the dispatch mail - * that woke the turn. Undefined for any other event. */ -export type MessageRunBracket = { - readonly messageId: string; - readonly messageRunId: string; -}; - -export function messageRunBracket(event: unknown): MessageRunBracket | undefined { - if (typeof event !== "object" || event === null) return undefined; - const type = (event as { type?: unknown }).type; - if (type !== "message.run.started" && type !== "message.run.ended") { - return undefined; - } - const data = (event as { data?: { messageId?: unknown; messageRunId?: unknown } }).data; - if (typeof data?.messageId !== "string" || typeof data?.messageRunId !== "string") { - return undefined; - } - return { messageId: data.messageId, messageRunId: data.messageRunId }; -} - -/** A `message.run.started` bracket open — the harness's own per-message - * start signal, minted fresh (`messageRunId`) for every dequeued - * message, including a redelivery of the same `messageId` — or - * undefined for any other event. Chat's orchestrator uses this to - * re-arm its silent-turn notice per turn rather than only on a real - * reply (see `chat-orchestrator.ts`'s `notifiedDropAddresses`). */ -export function messageRunStarted(event: unknown): boolean { - return ( - typeof event === "object" && - event !== null && - (event as { type?: unknown }).type === "message.run.started" - ); -} - -/** A `message.run.ended` bracket close — the harness's own per-message - * terminal signal (`status: "completed" | "failed"`) — or undefined for - * any other event. */ -export function messageRunEnded(event: unknown): MessageRunEnded | undefined { - if ( - typeof event !== "object" || - event === null || - (event as { type?: unknown }).type !== "message.run.ended" - ) { - return undefined; - } - const data = (event as { data?: { status?: unknown; error?: { message?: unknown } } }).data; - if (data?.status !== "completed" && data?.status !== "failed") { - return undefined; - } - const errorMessage = typeof data.error?.message === "string" ? data.error.message : undefined; - return { status: data.status, errorMessage }; -} diff --git a/packages/agent-events/src/index.ts b/packages/agent-events/src/index.ts deleted file mode 100644 index 9f3225c4af..0000000000 --- a/packages/agent-events/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - connectorReplyContent, - inferenceDoneBlocks, - toolDoneResult, - messageRunBracket, - messageRunStarted, - messageRunEnded, - type MessageRunBracket, - type MessageRunEnded, - type ReplyContentBlock, -} from "./agent-events"; diff --git a/packages/agent-events/tsconfig.json b/packages/agent-events/tsconfig.json deleted file mode 100644 index e125e90614..0000000000 --- a/packages/agent-events/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src"], - "exclude": [] -} diff --git a/packages/agent-events/tsconfig.src.json b/packages/agent-events/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/packages/agent-events/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/packages/artifact-ui/LICENSE b/packages/artifact-ui/LICENSE deleted file mode 100644 index 173c3a21d0..0000000000 --- a/packages/artifact-ui/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! diff --git a/packages/artifact-ui/README.md b/packages/artifact-ui/README.md deleted file mode 100644 index 71b1fb2e07..0000000000 --- a/packages/artifact-ui/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# @corbits/artifact-ui - -The artifact library's domain seam: the `ArtifactSummary` type a Library -page renders, plus the sort, filter, kind-coloring, and typed rendering -rules every artifact surface shares — the canvas pane, the Library detail -preview, and an opened chat blob all render through the same -`ArtifactRenderer` so a kind's display has one implementation. -Presentational primitives come from `@corbits/react-ui` -([corbitsdev/react-ui](https://github.com/corbitsdev/react-ui)); this -package holds the workbench-specific composition and domain rules on top -of them. - -Rendering is currently read-only; the multiplayer-editing half -is separate follow-up work built on top of this package's substrate. - -## Key modules - -- `types.ts` — `ArtifactSummary`, the arktype-validated row a Library page - renders (today mapped from tenant assets; a dedicated artifacts endpoint - would validate against this same schema) -- `sort-filter.ts` / `kind-filter.ts` — `sortArtifacts`/`filterArtifacts` - and the Library-path kind-segment rules -- `renderer-kind.ts` — `resolveArtifactRendererKind`, mapping an artifact - to its `ArtifactRendererKind` -- `artifact-renderer.tsx` — `ArtifactRenderer`, the typed dispatch to one - renderer per kind -- `artifact-card.tsx` — the Library card, including the kind badge -- `artifact-text-editor.tsx` / `y-text-diff.ts` — the text editor surface - and its Yjs-backed diff application -- `save-state.ts` — save-state label formatting - -## Running tests - -``` -cd packages/artifact-ui && bun test -``` - -Suites render with `react-dom/server`'s `renderToStaticMarkup` rather than -a mounted DOM, so no `bunfig.toml` preload is needed here. diff --git a/packages/artifact-ui/package.json b/packages/artifact-ui/package.json deleted file mode 100644 index f5eb61c6cb..0000000000 --- a/packages/artifact-ui/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@corbits/artifact-ui", - "version": "0.0.1", - "private": true, - "description": "The artifact library's server-reachable seam: the ArtifactSummary type and the Files kind-nav mapping the hub uses to compute per-kind counts", - "license": "LGPL-2.1-or-later", - "type": "module", - "sideEffects": false, - "exports": { - ".": "./src/types.ts", - "./kind-filter": "./src/kind-filter.ts", - "./title-extension": "./src/title-extension.ts", - "./types": "./src/types.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "@corbits/url-path": "workspace:*", - "arktype": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - } -} diff --git a/packages/artifact-ui/tsconfig.json b/packages/artifact-ui/tsconfig.json deleted file mode 100644 index 3c80a88bc7..0000000000 --- a/packages/artifact-ui/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src"], - "exclude": [], - "references": [ - { - "path": "../url-path/tsconfig.src.json" - } - ] -} diff --git a/packages/artifact-ui/tsconfig.src.json b/packages/artifact-ui/tsconfig.src.json deleted file mode 100644 index 851531bc4d..0000000000 --- a/packages/artifact-ui/tsconfig.src.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "lib": ["ESNext"], - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - }, - "references": [ - { - "path": "../url-path/tsconfig.src.json" - } - ] -} diff --git a/packages/tool-registry-publish/LICENSE b/packages/tool-registry-publish/LICENSE deleted file mode 100644 index 173c3a21d0..0000000000 --- a/packages/tool-registry-publish/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; version 2.1 of the License. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! diff --git a/packages/tool-registry-publish/README.md b/packages/tool-registry-publish/README.md deleted file mode 100644 index ac9cc70d49..0000000000 --- a/packages/tool-registry-publish/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# @corbits/tool-registry-publish - -Packs a `@corbits/*-tools` package into a self-contained npm-style -tarball (every dependency inlined, so the tarball declares no -`dependencies` at all) and publishes it into a tenant's `corbits-tools` -`package-registry` asset over the hub's native asset REST routes — -the packaging pipeline `@corbits/*` tool-package pins need to resolve -at agent launch (see `vendor/intx/hub-sessions/src/package-registry-kind.ts` -for the asset's shape and `@intx/tool-packaging/src/loader.ts` -for how a pin resolves through it). - -## Contract - -**Owns:** - -- `CORBITS_TOOLS_REGISTRY` — the one literal naming the registry asset; - `apps/hub`'s scope-routing config imports this constant rather than - repeating the string. -- `CORBITS_TOOL_PACKAGE_DIRS` — the list of `packages/*-tools` - directories this publisher packs and pushes. A package whose pin has - no entry here fails the closure resolver with "unknown registry" at - launch; registering it here is the fix. -- `packToolPackageTarball` — reads a package directory's - `package.json`, bundles its `"."` export with the `bun build` CLI - (an isolated subprocess — an in-process `Bun.build()` call was - observed to fail nondeterministically alongside other live `bun` - processes), enumerates the bundled tool factories into a - `ToolSurfaceManifest` (`qualifiedId`/`kind`/optional `ask` approval, - duplicate-qualifiedId-rejecting, arktype-parsed), and tars the result - with that manifest written into the synthesized `package.json`. - Memoized per directory for a process's lifetime, so concurrent or - repeated calls never race two bundler invocations against the same - input. -- `readToolSurfaceManifests` — reads those manifests back out of a - packed `tarballs/*.tgz` blob tree with injected `listBlobs`/`readBlob` - (the hub wires them to its launch-path `assetService` wrapper). The - hub's pinned-tool grants derive from this installed manifest — the - source-importing describer this replaces is gone, so the hub never - imports tool-package sources to grant. -- `publishCorbitsToolsRegistry` — find-or-create the tenant's - `corbits-tools` asset (409-tolerant, so two overlapping publish runs - for the same tenant never both fail on the asset's own name - collision), then `PUT` every tarball `CORBITS_TOOL_PACKAGE_DIRS` - produces. `workbench setup` is the product caller (root tenant; - descendants inherit). Freshness (`src/` moved without a version bump) - fails this publish, not bench create. - -**A host injects:** - -- `api` (an `ApiCall`: `(method, path, body?, cookies?) => Promise<{status, data, cookies}>`), - `cookies`, and `hubUrl` — this package issues no request without - them and holds no session or authentication state of its own. -- `tenantId` — which tenant's registry asset to publish into; this - package has no notion of "the" tenant. -- `log` — a plain `(line: string) => void` sink for progress lines. -- `fetchImpl` (optional) — the raw `fetch` the tarball `PUT` uses, - defaulting to the global `fetch`; a caller substitutes it the same - way it would substitute `api`. - -**Never imports:** - -- Any hub API client package — a caller supplies its own `fetch`-shaped - `ApiCall`, so this package declares that type structurally rather than - importing it from anywhere. -- `HubApiError` or any operator-facing error-wrapping convention — every - failure here is a plain `Error`; wrapping it as an actionable - `HubApiError` (problem + fix) is the calling setup step's job, not this - package's. -- Any workflow definition or `DEFAULT_WORKFLOWS` — this package knows - which tool packages to publish, never which workflows pin them. - -## End state - -This package stays pack + explicit publish: no boot-time publish (the -hub only reads the registry; `seedTenant` never packs), no daemon or -freshness loop (freshness is a `check:structural` gate and a publish -precondition, never a background task), no registry table (the registry -is a `package-registry` asset, not a SQL row), and pins resolve on -immutable `name@version` tarball filenames. Full deletion is blocked -until `@corbits/seeding`, `@workbench/onboarding`, and -`scripts/checks/tool-package-freshness` migrate off this package's -library entry (their lanes own those moves). - -## Running tests - -```sh -cd packages/tool-registry-publish && bun test -``` - -Tests exercise `packToolPackageTarball` against real package directories -under `packages/*-tools`; no `DATABASE_URL` is required. diff --git a/packages/tool-registry-publish/package.json b/packages/tool-registry-publish/package.json deleted file mode 100644 index 1099927b36..0000000000 --- a/packages/tool-registry-publish/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@corbits/tool-registry-publish", - "version": "0.0.1", - "private": true, - "description": "Packs workbench @corbits/*-tools packages into self-contained npm-style tarballs and publishes them into a tenant's corbits-tools package-registry asset, so tool-package pins under the @corbits scope resolve at agent launch", - "license": "LGPL-2.1-only", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test", - "check:freshness": "bun src/freshness-check.ts" - }, - "dependencies": { - "@intx/inference": "workspace:*", - "@intx/log": "0.3.0", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "tar": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - } -} diff --git a/packages/tool-registry-publish/src/freshness-check.test.ts b/packages/tool-registry-publish/src/freshness-check.test.ts deleted file mode 100644 index 154d16fe31..0000000000 --- a/packages/tool-registry-publish/src/freshness-check.test.ts +++ /dev/null @@ -1,458 +0,0 @@ -// Proves a tool package whose `src/` moved without a version bump fails -// loud — the failure mode `publishCorbitsToolsRegistry` cannot catch, -// because it skips an already-published name@version rather than -// comparing source. Fixtures are scratch git repos so this suite does -// not depend on the worktree's own dirty tool packages. History is -// written with plumbing (`commit-tree`) under a test-owned git config -// so a developer `core.hooksPath` cannot fail the suite. -import { afterAll, describe, expect, test } from "bun:test"; -import { chmod, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { CORBITS_TOOL_PACKAGE_DIRS } from "./registry"; -import { - assertToolPackagesFresh, - checkToolPackageFreshness, - snapshotToolPackages, - staleToolPackages, - StaleToolPackageError, -} from "./freshness-check"; - -const scratchDirs: string[] = []; -let fixtureGitConfig: string | undefined; -let repositoryRoot: string | undefined; - -afterAll(async () => { - await Promise.all(scratchDirs.map((dir) => rm(dir, { recursive: true, force: true }))); -}); - -async function findRepositoryRoot(): Promise { - if (repositoryRoot === undefined) { - const proc = Bun.spawn(["git", "rev-parse", "--show-toplevel"], { - cwd: process.cwd(), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, code] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, - ]); - if (code !== 0) { - throw new Error(`git rev-parse --show-toplevel failed: ${stderr}`); - } - repositoryRoot = await realpath(stdout.trim()); - } - return repositoryRoot; -} - -// Every fixture repo this suite writes must live outside the repository's -// own work tree — a git-fixture test must never be able to -// touch real history, however indirectly. -async function assertOutsideRepository(dir: string): Promise { - const resolved = await realpath(dir); - const root = await findRepositoryRoot(); - if (resolved === root || resolved.startsWith(`${root}${path.sep}`)) { - throw new Error( - `git fixture directory ${resolved} is inside the repository root ` + - `${root} — fixtures must live in an mkdtemp scratch dir outside ` + - `any work tree.`, - ); - } -} - -async function scratchDir(prefix: string): Promise { - const dir = await mkdtemp(path.join(tmpdir(), prefix)); - await assertOutsideRepository(dir); - scratchDirs.push(dir); - return dir; -} - -async function fixtureGitEnv(): Promise> { - if (fixtureGitConfig === undefined) { - const dir = await scratchDir("corbits-tools-freshness-gitconfig-"); - const hooks = path.join(dir, "hooks"); - await mkdir(hooks); - fixtureGitConfig = path.join(dir, "config"); - await writeFile( - fixtureGitConfig, - [ - "[user]", - "\tname = Freshness", - "\temail = freshness@test", - "[core]", - `\thooksPath = ${hooks}`, - "[commit]", - "\tgpgsign = false", - "", - ].join("\n"), - ); - } - const env = { ...process.env }; - delete env["GIT_DIR"]; - delete env["GIT_WORK_TREE"]; - delete env["GIT_INDEX_FILE"]; - return { - ...env, - GIT_CONFIG_NOSYSTEM: "1", - GIT_CONFIG_GLOBAL: fixtureGitConfig, - }; -} - -async function git( - cwd: string, - args: readonly string[], - env?: Record, -): Promise { - const proc = Bun.spawn( - [ - "git", - "-c", - "core.hooksPath=", - "-c", - "user.email=freshness@test", - "-c", - "user.name=Freshness", - "-c", - "commit.gpgsign=false", - ...args, - ], - { - cwd, - env: env ?? (await fixtureGitEnv()), - stdout: "pipe", - stderr: "pipe", - }, - ); - const [stdout, stderr, code] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, - ]); - if (code !== 0) { - throw new Error(`git ${args.join(" ")} failed: ${stderr}`); - } - return stdout.trim(); -} - -async function rawGit( - cwd: string, - args: readonly string[], - env: Record, -): Promise { - const proc = Bun.spawn(["git", ...args], { - cwd, - env, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, code] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, - ]); - if (code !== 0) { - throw new Error(`git ${args.join(" ")} failed: ${stderr}`); - } - return stdout.trim(); -} - -// `git commit` runs the operator's commit-msg / author hooks. Plumbing -// writes the same history without that surface, so this suite does not -// depend on whoever owns `core.hooksPath` on the machine. -async function commitAll( - root: string, - message: string, - env?: Record, -): Promise { - await git(root, ["add", "."], env); - const tree = await git(root, ["write-tree"], env); - let parent: string | undefined; - try { - parent = await git(root, ["rev-parse", "--verify", "HEAD"], env); - } catch { - parent = undefined; - } - const sha = - parent === undefined - ? await git(root, ["commit-tree", tree, "-m", message], env) - : await git(root, ["commit-tree", tree, "-p", parent, "-m", message], env); - await git(root, ["update-ref", "HEAD", sha], env); -} - -async function writePkg(pkg: string, version: string, src: string): Promise { - await mkdir(path.join(pkg, "src"), { recursive: true }); - await writeFile( - path.join(pkg, "package.json"), - `${JSON.stringify({ name: "@corbits/fake-tools", version }, null, 2)}\n`, - ); - await writeFile(path.join(pkg, "src", "index.ts"), src); -} - -async function committedPackage( - src: string, - env?: Record, -): Promise<{ - root: string; - pkg: string; -}> { - const root = await scratchDir("corbits-tools-freshness-"); - await git(root, ["init", "-b", "main"], env); - const pkg = path.join(root, "fake-tools"); - await writePkg(pkg, "0.0.1", src); - await commitAll(root, "initial @corbits/fake-tools@0.0.1", env); - return { root, pkg }; -} - -async function plantRejectingAuthorHook(): Promise { - const work = await scratchDir("corbits-tools-freshness-hooks-"); - const hooksDir = path.join(work, "hooks"); - await mkdir(hooksDir); - const hook = path.join(hooksDir, "commit-msg"); - await writeFile( - hook, - [ - "#!/bin/sh", - 'echo "commit blocked: author must be listed in allowed-emails" >&2', - "exit 1", - "", - ].join("\n"), - ); - await chmod(hook, 0o755); - const globalConfig = path.join(work, "gitconfig"); - await writeFile(globalConfig, `[core]\nhooksPath = ${hooksDir}\n`); - return globalConfig; -} - -describe("staleToolPackages", () => { - test("src-changed-without-bump is a finding", () => { - expect( - staleToolPackages([ - { - name: "@corbits/fake-tools", - dir: "/tmp/fake-tools", - currentVersion: "0.0.1", - publishedVersion: "0.0.1", - srcChangedSincePublished: true, - }, - ]), - ).toEqual([ - { - name: "@corbits/fake-tools", - dir: "/tmp/fake-tools", - version: "0.0.1", - }, - ]); - }); - - test("a version bump clears the finding", () => { - expect( - staleToolPackages([ - { - name: "@corbits/fake-tools", - dir: "/tmp/fake-tools", - currentVersion: "0.0.2", - publishedVersion: "0.0.1", - srcChangedSincePublished: true, - }, - ]), - ).toEqual([]); - }); - - test("unchanged src at the published version is fresh", () => { - expect( - staleToolPackages([ - { - name: "@corbits/fake-tools", - dir: "/tmp/fake-tools", - currentVersion: "0.0.1", - publishedVersion: "0.0.1", - srcChangedSincePublished: false, - }, - ]), - ).toEqual([]); - }); - - test("a package with no published version is not stale", () => { - expect( - staleToolPackages([ - { - name: "@corbits/fake-tools", - dir: "/tmp/fake-tools", - currentVersion: "0.0.1", - publishedVersion: undefined, - srcChangedSincePublished: true, - }, - ]), - ).toEqual([]); - }); -}); - -describe("assertToolPackagesFresh", () => { - test("src-changed-without-bump is loud", () => { - const snapshots = [ - { - name: "@corbits/fake-tools", - dir: "/tmp/fake-tools", - currentVersion: "0.0.1", - publishedVersion: "0.0.1", - srcChangedSincePublished: true, - }, - ] as const; - - expect(() => assertToolPackagesFresh(snapshots)).toThrow(StaleToolPackageError); - - try { - assertToolPackagesFresh(snapshots); - throw new Error("expected StaleToolPackageError"); - } catch (err) { - expect(err).toBeInstanceOf(StaleToolPackageError); - const message = (err as StaleToolPackageError).message; - expect(message).toContain("@corbits/fake-tools@0.0.1"); - expect(message).toContain("src/"); - expect(message).toContain("without bumping version"); - expect(message).toContain("name@version"); - expect(message).toContain("Bump the package.json version"); - expect(message).toContain("/tmp/fake-tools"); - } - }); -}); - -describe("checkToolPackageFreshness", () => { - test("src-changed-without-bump is loud against a real git tree", async () => { - const { pkg } = await committedPackage("export const n = 1;\n"); - await writePkg(pkg, "0.0.1", "export const n = 2;\n"); - - let thrown: unknown; - try { - await checkToolPackageFreshness({ packageDirs: [pkg] }); - } catch (err) { - thrown = err; - } - expect(thrown).toBeInstanceOf(StaleToolPackageError); - const message = (thrown as StaleToolPackageError).message; - expect(message).toContain("@corbits/fake-tools@0.0.1"); - expect(message).toContain("src/"); - expect(message).toContain("without bumping version"); - expect(message).toContain("Bump the package.json version"); - }); - - test("a new untracked src file without a bump is loud", async () => { - const { pkg } = await committedPackage("export const n = 1;\n"); - await writeFile(path.join(pkg, "src", "extra.ts"), "export const extra = 1;\n"); - - await expect(checkToolPackageFreshness({ packageDirs: [pkg] })).rejects.toBeInstanceOf( - StaleToolPackageError, - ); - }); - - test("src changed together with a version bump is fresh", async () => { - const { pkg } = await committedPackage("export const n = 1;\n"); - await writePkg(pkg, "0.0.2", "export const n = 2;\n"); - await checkToolPackageFreshness({ packageDirs: [pkg] }); - }); - - test("unchanged src at the published version is fresh", async () => { - const { pkg } = await committedPackage("export const n = 1;\n"); - await checkToolPackageFreshness({ packageDirs: [pkg] }); - }); - - test.each([ - ["GIT_INDEX_FILE", ".git/index"], - ["GIT_DIR", ".git"], - ["GIT_WORK_TREE", "."], - ["GIT_COMMON_DIR", ".git"], - ["GIT_OBJECT_DIRECTORY", ".git/objects"], - ])("ignores a foreign %s while detecting real source changes", async (key, suffix) => { - const foreign = await committedPackage("export const n = 99;\n"); - const { pkg } = await committedPackage("export const n = 1;\n"); - const inherited = process.env; - process.env = { ...inherited, [key]: path.join(foreign.root, suffix) }; - try { - const snapshots = await snapshotToolPackages([pkg]); - expect(snapshots).toEqual([ - { - name: "@corbits/fake-tools", - dir: pkg, - currentVersion: "0.0.1", - publishedVersion: "0.0.1", - srcChangedSincePublished: false, - }, - ]); - await checkToolPackageFreshness({ packageDirs: [pkg] }); - await writePkg(pkg, "0.0.1", "export const n = 2;\n"); - await expect(checkToolPackageFreshness({ packageDirs: [pkg] })).rejects.toBeInstanceOf( - StaleToolPackageError, - ); - } finally { - process.env = inherited; - } - }); - - test("committed src change after the version-introducing commit is loud", async () => { - const { root, pkg } = await committedPackage("export const n = 1;\n"); - await writePkg(pkg, "0.0.1", "export const n = 2;\n"); - await commitAll(root, "src change, forgot the bump"); - - await expect(checkToolPackageFreshness({ packageDirs: [pkg] })).rejects.toBeInstanceOf( - StaleToolPackageError, - ); - }); - - test("fixture history is written even when a global author hook would reject git commit", async () => { - const globalConfig = await plantRejectingAuthorHook(); - const baseEnv = { ...process.env }; - delete baseEnv["GIT_DIR"]; - delete baseEnv["GIT_WORK_TREE"]; - delete baseEnv["GIT_INDEX_FILE"]; - const hostileEnv = { - ...baseEnv, - GIT_CONFIG_NOSYSTEM: "1", - GIT_CONFIG_GLOBAL: globalConfig, - }; - - const victim = await scratchDir("corbits-tools-freshness-victim-"); - await rawGit( - victim, - ["-c", "user.email=freshness@test", "-c", "user.name=Freshness", "init", "-b", "main"], - hostileEnv, - ); - await writeFile(path.join(victim, "README"), "victim\n"); - await rawGit(victim, ["add", "."], hostileEnv); - let unguarded: unknown; - try { - await rawGit( - victim, - [ - "-c", - "user.email=freshness@test", - "-c", - "user.name=Freshness", - "commit", - "-m", - "should be blocked", - ], - hostileEnv, - ); - } catch (err) { - unguarded = err; - } - expect(unguarded).toBeInstanceOf(Error); - expect((unguarded as Error).message).toContain("allowed-emails"); - - const { pkg } = await committedPackage("export const n = 1;\n", hostileEnv); - await checkToolPackageFreshness({ packageDirs: [pkg] }); - }); -}); - -describe("snapshotToolPackages", () => { - test("defaults to the publish map", async () => { - const snapshots = await snapshotToolPackages(); - expect(snapshots.map((snapshot) => snapshot.dir)).toEqual([...CORBITS_TOOL_PACKAGE_DIRS]); - expect(snapshots.length).toBe(CORBITS_TOOL_PACKAGE_DIRS.length); - for (const snapshot of snapshots) { - expect(snapshot.name.startsWith("@corbits/")).toBe(true); - expect(snapshot.currentVersion.length).toBeGreaterThan(0); - } - }); -}); diff --git a/packages/tool-registry-publish/src/freshness-check.ts b/packages/tool-registry-publish/src/freshness-check.ts deleted file mode 100644 index 33875a792a..0000000000 --- a/packages/tool-registry-publish/src/freshness-check.ts +++ /dev/null @@ -1,252 +0,0 @@ -// Fails loud when a `@corbits/*-tools` package's `src/` has moved on -// from the version already committed (the version seed will publish and -// that pins resolve as `name@version`). Publish skips an already-present -// filename and tar bytes are not deterministic, so a forgotten bump -// would otherwise ship nothing — running agents keep the old tools. -// Compared against git, not the live registry: the committed -// `package.json` version is the pin. - -import { readFile, realpath } from "node:fs/promises"; -import path from "node:path"; -import { type } from "arktype"; -import { getLogger } from "@intx/log"; -import { CORBITS_TOOL_PACKAGE_DIRS } from "./registry"; - -const log = getLogger(["tool-registry-publish", "freshness-check"]); - -const PackageManifest = type({ - name: "string", - version: "string", -}); - -export type ToolPackageSnapshot = { - name: string; - dir: string; - currentVersion: string; - publishedVersion: string | undefined; - srcChangedSincePublished: boolean; -}; - -export type StaleToolPackage = { - name: string; - dir: string; - version: string; -}; - -export type CheckToolPackageFreshnessArgs = { - packageDirs?: readonly string[]; -}; - -/** - * Packages whose `src/` differs from the tree at the last commit that - * introduced `currentVersion`, while that version is still the one in - * the working-tree manifest. A version bump (working tree or committed) - * clears the finding — that is the only supported way to ship new - * tool-package bytes. - */ -export function staleToolPackages(snapshots: readonly ToolPackageSnapshot[]): StaleToolPackage[] { - const stale: StaleToolPackage[] = []; - for (const snapshot of snapshots) { - if (snapshot.publishedVersion === undefined) continue; - if (snapshot.currentVersion !== snapshot.publishedVersion) continue; - if (!snapshot.srcChangedSincePublished) continue; - stale.push({ - name: snapshot.name, - dir: snapshot.dir, - version: snapshot.currentVersion, - }); - } - return stale; -} - -export class StaleToolPackageError extends Error { - readonly stale: readonly StaleToolPackage[]; - - constructor(stale: readonly StaleToolPackage[]) { - const names = stale.map((pkg) => `${pkg.name}@${pkg.version}`).join(", "); - super( - `tool-package freshness: ${names} changed src/ without bumping version. ` + - `Tool-package resolution keys on name@version, so shipping new source ` + - `under an unchanged version never reaches a running or freshly-launched ` + - `agent. Bump the package.json version before republishing.\n` + - stale.map((pkg) => ` ${pkg.dir}`).join("\n"), - ); - this.name = "StaleToolPackageError"; - this.stale = stale; - } -} - -export function assertToolPackagesFresh(snapshots: readonly ToolPackageSnapshot[]): void { - const stale = staleToolPackages(snapshots); - if (stale.length > 0) throw new StaleToolPackageError(stale); -} - -export async function snapshotToolPackages( - packageDirs: readonly string[] = CORBITS_TOOL_PACKAGE_DIRS, -): Promise { - return Promise.all(packageDirs.map(snapshotOnePackage)); -} - -/** - * Snapshot every registered tool package and throw - * `StaleToolPackageError` if any `src/` moved without a version bump. - * `packageDirs` defaults to `CORBITS_TOOL_PACKAGE_DIRS` — the same - * publish map `publishCorbitsToolsRegistry` walks. - */ -export async function checkToolPackageFreshness( - args: CheckToolPackageFreshnessArgs = {}, -): Promise { - const snapshots = await snapshotToolPackages(args.packageDirs ?? CORBITS_TOOL_PACKAGE_DIRS); - assertToolPackagesFresh(snapshots); -} - -async function snapshotOnePackage(dir: string): Promise { - const resolvedDir = await realpath(dir); - const manifest = await readManifest(path.join(resolvedDir, "package.json")); - const root = await repoRootFor(resolvedDir); - if (root === undefined) { - throw new Error( - `tool-package freshness: ${dir} is not inside a git work tree; ` + - `src/ cannot be compared to the published version`, - ); - } - - const resolvedRoot = await realpath(root); - const relManifest = gitPath(resolvedRoot, path.join(resolvedDir, "package.json")); - const relSrc = gitPath(resolvedRoot, path.join(resolvedDir, "src")); - const publishedVersion = await committedVersion(root, relManifest); - const srcChangedSincePublished = - publishedVersion !== undefined && - publishedVersion === manifest.version && - (await srcChangedSinceVersion(root, relManifest, relSrc, manifest.version)); - - return { - name: manifest.name, - dir, - currentVersion: manifest.version, - publishedVersion, - srcChangedSincePublished, - }; -} - -async function readManifest(manifestPath: string): Promise<{ name: string; version: string }> { - return parseManifest(await readFile(manifestPath, "utf8"), manifestPath); -} - -function parseManifest(jsonText: string, label: string): { name: string; version: string } { - let raw: unknown; - try { - raw = JSON.parse(jsonText); - } catch (err) { - throw new Error(`tool-package freshness: ${label} is not valid JSON: ${String(err)}`, { - cause: err, - }); - } - const parsed = PackageManifest(raw); - if (parsed instanceof type.errors) { - throw new Error(`tool-package freshness: ${label} failed validation: ${parsed.summary}`); - } - return parsed; -} - -async function committedVersion(root: string, relManifest: string): Promise { - const shown = await git(root, ["show", `HEAD:${relManifest}`]); - if (shown.code !== 0) return undefined; - return parseManifest(shown.stdout, `HEAD:${relManifest}`).version; -} - -/** - * True when `relSrc` differs from the oldest commit that still carries - * `version` in `relManifest` (the bump that introduced it), including - * uncommitted and untracked files. Walking `git log` of the manifest - * rather than pickaxe-searching the version string so a reformatted - * `package.json` cannot hide a forgotten bump. - */ -async function srcChangedSinceVersion( - root: string, - relManifest: string, - relSrc: string, - version: string, -): Promise { - const since = await lastCommitIntroducingVersion(root, relManifest, version); - if (since === undefined) return false; - const diff = await git(root, ["diff", "--name-only", since, "--", relSrc]); - if (diff.stdout.trim() !== "") return true; - const untracked = await git(root, ["ls-files", "--others", "--exclude-standard", "--", relSrc]); - return untracked.stdout.trim() !== ""; -} - -async function lastCommitIntroducingVersion( - root: string, - relManifest: string, - version: string, -): Promise { - const log = await git(root, ["log", "--format=%H", "--", relManifest]); - if (log.code !== 0 || log.stdout.trim() === "") return undefined; - let oldestWithVersion: string | undefined; - for (const commit of log.stdout.split("\n").filter(Boolean)) { - const shown = await git(root, ["show", `${commit}:${relManifest}`]); - if (shown.code !== 0) continue; - const parsed = parseManifest(shown.stdout, `${commit}:${relManifest}`); - if (parsed.version !== version) break; - oldestWithVersion = commit; - } - return oldestWithVersion; -} - -async function repoRootFor(dir: string): Promise { - const result = await git(dir, ["rev-parse", "--show-toplevel"]); - if (result.code !== 0) return undefined; - return result.stdout.trim(); -} - -function gitPath(root: string, absolute: string): string { - return path.relative(root, absolute).split(path.sep).join("/"); -} - -async function git( - cwd: string, - args: readonly string[], -): Promise<{ code: number; stdout: string; stderr: string }> { - if (Bun.which("git") === null) { - throw new Error( - "tool-package freshness: git is not installed or not on PATH; " + - "src/ cannot be compared to the published version", - ); - } - // Hooks export repository selectors; cwd must select the package's repo, - // not the index or object database of the repository running the hook. - const repositoryEnvKeys = new Set([ - "GIT_DIR", - "GIT_WORK_TREE", - "GIT_COMMON_DIR", - "GIT_INDEX_FILE", - "GIT_OBJECT_DIRECTORY", - "GIT_ALTERNATE_OBJECT_DIRECTORIES", - "GIT_PREFIX", - ]); - const env = Object.fromEntries( - Object.entries(process.env).filter(([key]) => !repositoryEnvKeys.has(key)), - ); - const proc = Bun.spawn(["git", ...args], { - cwd, - env, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, code] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, - ]); - return { code, stdout: stdout.trimEnd(), stderr: stderr.trimEnd() }; -} - -if (import.meta.main) { - try { - await checkToolPackageFreshness(); - } catch (err) { - log.error`${err instanceof Error ? err.message : String(err)}`; - process.exit(1); - } -} diff --git a/packages/tool-registry-publish/src/index.ts b/packages/tool-registry-publish/src/index.ts deleted file mode 100644 index f3bdcddf2a..0000000000 --- a/packages/tool-registry-publish/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -export { - CORBITS_TOOLS_REGISTRY, - CORBITS_TOOL_PACKAGE_DIRS, - REQUIRED_SEED_TOOL_PACKAGES, - tarballCoversPackage, - tarballsCoverRequiredSeedPackages, -} from "./registry"; -export { packToolPackageTarball, tarballFilenameFor, type PackedTarball } from "./pack"; -export { ToolSurfaceEntry, ToolSurfaceManifest } from "./manifest"; -export { readToolSurfaceManifests, type ToolSurfaceBlobSource } from "./surface-reader"; -export { - shouldPublishTarball, - publishCorbitsToolsRegistry, - isCorbitsToolsRegistrySeeded, - sha512Integrity, - fetchRegistryTarballSource, - installRegistryTarball, - EmptyRegistryPublishError, - type ApiCall, - type ApiResult, - type FetchTarballPut, - type FetchTarballSource, - type PublishCorbitsToolsRegistryArgs, - type PublishCorbitsToolsRegistryResult, - type PublishSummary, -} from "./publish"; -export { - checkToolPackageFreshness, - staleToolPackages, - StaleToolPackageError, - type CheckToolPackageFreshnessArgs, - type StaleToolPackage, - type ToolPackageSnapshot, -} from "./freshness-check"; diff --git a/packages/tool-registry-publish/src/manifest.test.ts b/packages/tool-registry-publish/src/manifest.test.ts deleted file mode 100644 index f1a4e21873..0000000000 --- a/packages/tool-registry-publish/src/manifest.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { type } from "arktype"; -import { ToolSurfaceManifest } from "./manifest"; - -describe("ToolSurfaceManifest", () => { - const valid = { - name: "@corbits/memory-tools", - version: "0.0.1", - surface: [ - { - qualifiedId: "@corbits/memory-tools/memory:memory_add", - kind: "tool", - approval: "ask", - }, - { - qualifiedId: "@corbits/memory-tools/memory:memory_list", - kind: "tool", - }, - ], - }; - - test("accepts a real surface", () => { - const manifest = ToolSurfaceManifest(valid); - expect(manifest).not.toBeInstanceOf(type.errors); - if (!(manifest instanceof type.errors)) { - expect(manifest.name).toBe("@corbits/memory-tools"); - expect(manifest.surface).toHaveLength(2); - } - }); - - test("rejects a surface with duplicate qualifiedIds", () => { - const [first, second] = valid.surface; - if (first === undefined || second === undefined) throw new Error("fixture"); - const manifest = ToolSurfaceManifest({ - ...valid, - surface: [first, { ...second, qualifiedId: first.qualifiedId }], - }); - expect(manifest).toBeInstanceOf(type.errors); - }); - - test("rejects malformed entries", () => { - expect( - ToolSurfaceManifest({ - ...valid, - surface: [{ qualifiedId: "x", kind: "widget" }], - }), - ).toBeInstanceOf(type.errors); - expect(ToolSurfaceManifest({ ...valid, surface: [{ kind: "tool" }] })).toBeInstanceOf( - type.errors, - ); - expect(ToolSurfaceManifest({ ...valid, surface: "nope" })).toBeInstanceOf(type.errors); - }); - - test("accepts a skill-kind entry (manifest headroom)", () => { - const manifest = ToolSurfaceManifest({ - ...valid, - surface: [{ qualifiedId: "@corbits/skills/s:skills_load", kind: "skill" }], - }); - expect(manifest).not.toBeInstanceOf(type.errors); - }); -}); diff --git a/packages/tool-registry-publish/src/manifest.ts b/packages/tool-registry-publish/src/manifest.ts deleted file mode 100644 index c711dd8832..0000000000 --- a/packages/tool-registry-publish/src/manifest.ts +++ /dev/null @@ -1,36 +0,0 @@ -// The tool-surface manifest a packed `@corbits/*-tools` tarball carries -// in its synthesized `package.json`. The packer derives it from the -// package's own source at pack time; the hub reads it back out of the -// installed `corbits-tools` asset (see `./surface-reader.ts`) to mint -// `tool:` grants — no source imports on the hub side. -import { type } from "arktype"; - -export const ToolSurfaceEntry = type({ - qualifiedId: "string", - kind: "'tool' | 'skill'", - "approval?": "'ask'", -}); -export type ToolSurfaceEntry = typeof ToolSurfaceEntry.infer; - -// A qualifiedId is the binding/delivery key the workflow child's authz -// gate matches grants against, so a duplicate within one manifest is a -// defect the parse boundary must reject rather than collapse silently — -// the same invariant `ToolCredentialDeclarationArray` enforces for -// credential handles. -export const ToolSurfaceManifest = type({ - name: "string", - version: "string", - surface: ToolSurfaceEntry.array().narrow((entries, ctx) => { - const seen = new Set(); - for (const entry of entries) { - if (seen.has(entry.qualifiedId)) { - return ctx.mustBe( - `an array with no duplicate qualifiedIds; "${entry.qualifiedId}" appears more than once`, - ); - } - seen.add(entry.qualifiedId); - } - return true; - }), -}); -export type ToolSurfaceManifest = typeof ToolSurfaceManifest.infer; diff --git a/packages/tool-registry-publish/src/materialize.test.ts b/packages/tool-registry-publish/src/materialize.test.ts deleted file mode 100644 index 6cb957fc93..0000000000 --- a/packages/tool-registry-publish/src/materialize.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -// The nine `@corbits/*-tools` packages used to pack serially inside -// `publishCorbitsToolsRegistry`. This suite pins the helper that replaced -// that loop: independent work overlaps, a matching content hash can skip, -// and a changed package still rematerializes. -import { describe, expect, test } from "bun:test"; -import { sha512Integrity } from "./publish"; -import { materializePackages } from "./materialize"; - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -describe("materializePackages", () => { - test("runs independent materializes concurrently", async () => { - let inFlight = 0; - let maxInFlight = 0; - const release = Promise.withResolvers(); - let arrived = 0; - - const pending = materializePackages({ - packageDirs: ["a", "b", "c"], - concurrency: 3, - materialize: async (packageDir) => { - inFlight += 1; - maxInFlight = Math.max(maxInFlight, inFlight); - arrived += 1; - if (arrived === 3) release.resolve(); - await release.promise; - await delay(1); - inFlight -= 1; - return packageDir.toUpperCase(); - }, - }); - - const results = await pending; - expect(maxInFlight).toBe(3); - expect(results).toEqual([ - { packageDir: "a", status: "ok", value: "A" }, - { packageDir: "b", status: "ok", value: "B" }, - { packageDir: "c", status: "ok", value: "C" }, - ]); - }); - - test("caps the worker pool at the requested concurrency", async () => { - let inFlight = 0; - let maxInFlight = 0; - - await materializePackages({ - packageDirs: ["a", "b", "c", "d"], - concurrency: 2, - materialize: async (packageDir) => { - inFlight += 1; - maxInFlight = Math.max(maxInFlight, inFlight); - await delay(15); - inFlight -= 1; - return packageDir; - }, - }); - - expect(maxInFlight).toBe(2); - }); - - test("skips a package whose content hash already matches the on-disk materialize", async () => { - const encoder = new TextEncoder(); - const onDisk = new Map([["unchanged", sha512Integrity(encoder.encode("same-bytes"))]]); - const current = new Map([ - ["unchanged", sha512Integrity(encoder.encode("same-bytes"))], - ["changed", sha512Integrity(encoder.encode("new-bytes"))], - ]); - const materialized: string[] = []; - - const results = await materializePackages({ - packageDirs: ["unchanged", "changed"], - materialize: async (packageDir) => { - materialized.push(packageDir); - return packageDir; - }, - shouldSkip: (packageDir) => { - const existing = onDisk.get(packageDir); - const next = current.get(packageDir); - return existing !== undefined && next !== undefined && existing === next; - }, - }); - - expect(materialized).toEqual(["changed"]); - expect(results).toEqual([ - { packageDir: "unchanged", status: "skipped" }, - { packageDir: "changed", status: "ok", value: "changed" }, - ]); - }); - - test("a changed package still rematerializes when hashes differ", async () => { - const encoder = new TextEncoder(); - const existing = sha512Integrity(encoder.encode("old")); - const next = sha512Integrity(encoder.encode("new")); - expect(existing).not.toBe(next); - - const results = await materializePackages({ - packageDirs: ["pkg"], - materialize: async () => "rematerialized", - shouldSkip: () => existing === next, - }); - - expect(results).toEqual([{ packageDir: "pkg", status: "ok", value: "rematerialized" }]); - }); - - test("rejects when one materialize fails and does not swallow siblings", async () => { - const seen: string[] = []; - const pending = materializePackages({ - packageDirs: ["ok", "boom"], - concurrency: 2, - materialize: async (packageDir) => { - seen.push(packageDir); - if (packageDir === "boom") throw new Error("pack failed"); - return packageDir; - }, - }); - await expect(pending).rejects.toThrow("pack failed"); - expect(seen.sort()).toEqual(["boom", "ok"]); - }); - - test("returns an empty list for no packages", async () => { - const results = await materializePackages({ - packageDirs: [], - materialize: async () => { - throw new Error("should not materialize"); - }, - }); - expect(results).toEqual([]); - }); -}); diff --git a/packages/tool-registry-publish/src/materialize.ts b/packages/tool-registry-publish/src/materialize.ts deleted file mode 100644 index 6639f86673..0000000000 --- a/packages/tool-registry-publish/src/materialize.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Concurrent pack/materialize of independent `@corbits/*-tools` directories. -// The nine packages have no inter-pack dependency — each `bun build` is -// self-contained — so they can run together. A changed package still -// rematerializes: skip is opt-in and only fires when the caller already -// knows the content hash matches an on-disk materialize -// (`sha512Integrity` in `./publish.ts`). - -export type MaterializePackagesArgs = { - packageDirs: readonly string[]; - materialize: (packageDir: string) => Promise; - concurrency?: number; - shouldSkip?: (packageDir: string) => boolean | Promise; -}; - -export type MaterializedPackage = - | { packageDir: string; status: "skipped" } - | { packageDir: string; status: "ok"; value: T }; - -/** - * Run independent pack/materialize work across `packageDirs`. Results - * stay in input order. Default concurrency is one worker per directory - * (equivalent to `Promise.all`); pass `concurrency` to cap the pool. - */ -export async function materializePackages( - args: MaterializePackagesArgs, -): Promise[]> { - if (args.packageDirs.length === 0) return []; - const concurrency = args.concurrency ?? args.packageDirs.length; - if (concurrency < 1) { - throw new Error(`materializePackages: concurrency must be >= 1, got ${String(concurrency)}`); - } - return mapPool(args.packageDirs, concurrency, async (packageDir) => { - if (args.shouldSkip !== undefined && (await args.shouldSkip(packageDir))) { - return { packageDir, status: "skipped" }; - } - return { - packageDir, - status: "ok", - value: await args.materialize(packageDir), - }; - }); -} - -async function mapPool( - items: readonly T[], - concurrency: number, - fn: (item: T) => Promise, -): Promise { - const results: R[] = new Array(items.length); - let next = 0; - async function worker(): Promise { - for (;;) { - const index = next; - next += 1; - if (index >= items.length) return; - const item = items[index]; - if (item === undefined) return; - results[index] = await fn(item); - } - } - const workerCount = Math.min(concurrency, items.length); - const workers: Promise[] = []; - for (let i = 0; i < workerCount; i += 1) { - workers.push(worker()); - } - await Promise.all(workers); - return results; -} diff --git a/packages/tool-registry-publish/src/pack.test.ts b/packages/tool-registry-publish/src/pack.test.ts deleted file mode 100644 index ad708643d1..0000000000 --- a/packages/tool-registry-publish/src/pack.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import * as tar from "tar"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { type } from "arktype"; -import { CORBITS_TOOL_PACKAGE_DIRS, CORBITS_TOOLS_REGISTRY } from "./registry"; -import { packToolPackageTarball, tarballFilenameFor } from "./pack"; -import { ToolSurfaceManifest } from "./manifest"; - -// The kind handler's filename rule -// (vendor/intx/hub-sessions/src/package-registry-kind.ts -// `TARBALL_FILENAME_PATTERN`) — duplicated here so this test fails the -// moment a packed filename would be rejected by the substrate, without -// importing across the vendor boundary. -const TARBALL_FILENAME_PATTERN = /^[A-Za-z0-9_@+][A-Za-z0-9_.@+-]*\.tgz$/; - -describe("tarballFilenameFor", () => { - test("flattens a scoped package name and matches the registry's filename rule", () => { - const filename = tarballFilenameFor("@corbits/memory-tools", "0.0.1"); - expect(filename).toBe("corbits-memory-tools-0.0.1.tgz"); - expect(TARBALL_FILENAME_PATTERN.test(filename)).toBe(true); - }); -}); - -describe("packToolPackageTarball", () => { - test("registers at least one @corbits tool package", () => { - expect(CORBITS_TOOL_PACKAGE_DIRS.length).toBeGreaterThan(0); - }); - - for (const packageDir of CORBITS_TOOL_PACKAGE_DIRS) { - test(`packs ${path.basename(packageDir)} into a self-contained, validating tarball`, async () => { - const tarball = await packToolPackageTarball(packageDir); - - expect(tarball.filename).toBe(tarballFilenameFor(tarball.name, tarball.version)); - expect(TARBALL_FILENAME_PATTERN.test(tarball.filename)).toBe(true); - expect(tarball.bytes.byteLength).toBeGreaterThan(0); - - const extractDir = await mkdtemp(path.join(tmpdir(), "corbits-tools-pack-test-")); - try { - await Bun.write(path.join(extractDir, "out.tgz"), Buffer.from(tarball.bytes)); - await tar.extract({ - cwd: extractDir, - file: path.join(extractDir, "out.tgz"), - }); - - const pkgJsonPath = path.join(extractDir, "package", "package.json"); - const pkgJson = (await Bun.file(pkgJsonPath).json()) as { - name: string; - version: string; - interchange?: { tools?: string }; - surface?: unknown; - }; - expect(pkgJson.name).toBe(tarball.name); - expect(pkgJson.version).toBe(tarball.version); - expect(pkgJson.interchange?.tools).toBe("./tool.mjs"); - - // The synthesized package.json must carry a valid tool-surface - // manifest — the hub reads this, not the source tree, for grants. - const surface = ToolSurfaceManifest(pkgJson); - expect(surface).not.toBeInstanceOf(type.errors); - - // Loader parity: the manifest's qualifiedIds must equal exactly - // what the sidecar's tool loader sees when it import()s the - // packed bundle — `:` per definition - // (see `@intx/tool-packaging/src/loader.ts`'s - // `applyNamespacePrefix`). - const bundlePath = path.join(extractDir, "package", "tool.mjs"); - const mod = (await import(bundlePath)) as Record; - const factories = Object.values(mod).filter( - (value) => - typeof value === "function" && - typeof (value as { id?: unknown }).id === "string" && - Array.isArray((value as { definitions?: unknown }).definitions), - ); - expect(factories.length).toBeGreaterThan(0); - if (!(surface instanceof type.errors)) { - const loaderIds = factories.flatMap((factory) => - ( - factory as { - id: string; - definitions: { name: string }[]; - } - ).definitions.map( - (definition) => `${(factory as { id: string }).id}:${definition.name}`, - ), - ); - expect([...surface.surface.map((e) => e.qualifiedId)].sort()).toEqual( - [...loaderIds].sort(), - ); - } - - if (tarball.name === "@corbits/catalog-tools") { - // The single read-only `catalogTools` bundle must resolve by id - // from the packed tarball the sidecar actually loads, not just - // from the workspace source tree — a bundle dropped from - // `./index.ts`'s re-exports would still leave - // `factories.length` above 0. Catalog administration is UI-only - //, so there is no second bundle anymore. - const ids = factories.map((factory) => (factory as { id: string }).id); - expect(ids).toContain("@corbits/catalog-tools/catalog"); - expect(ids).not.toContain("@corbits/catalog-tools/off"); - } - } finally { - await rm(extractDir, { recursive: true, force: true }); - } - }); - } - - test(`${CORBITS_TOOLS_REGISTRY} is the registry these packages publish into`, () => { - // Documents the contract packages.test asserts against — a rename - // of the registry constant without updating this fixture set would - // still leave the tests above green, since they never spell the - // registry name; this is the one place that connects the two. - expect(CORBITS_TOOLS_REGISTRY).toBe("corbits-tools"); - }); -}); diff --git a/packages/tool-registry-publish/src/pack.ts b/packages/tool-registry-publish/src/pack.ts deleted file mode 100644 index c463693669..0000000000 --- a/packages/tool-registry-publish/src/pack.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Packs one `@corbits/*-tools` package directory into a self-contained -// npm-style tarball fit for a `package-registry` asset's `tarballs/` -// tree (vendor/intx/hub-sessions/src/package-registry-kind.ts). -// -// The source packages ship raw TypeScript behind a workspace `exports` -// field and depend on other workspace packages (`@intx/agent`, -// `@intx/types`, `arktype`, ...) via `workspace:`/`catalog:` specs that -// only resolve inside this monorepo. A tarball can carry neither, so -// each package's entry module is bundled with every dependency inlined -// into one plain ESM file — the tarball then declares no -// `dependencies` at all, and the closure resolver has nothing further -// to fetch. `interchange.tools` on the synthesized `package.json` -// points at that bundle, matching the shape -// `@intx/tool-packaging/src/loader.ts` reads. - -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { type } from "arktype"; -import * as tar from "tar"; -import { ToolSurfaceManifest, type ToolSurfaceEntry } from "./manifest"; - -const BUNDLE_ENTRY_FILENAME = "tool.mjs"; - -export type PackedTarball = { - name: string; - version: string; - filename: string; - bytes: Uint8Array; -}; - -/** The tarball filename convention `scripts/checks/packages.ts` uses for `bun pm pack` output, reused here for the packer's own tarballs. */ -export function tarballFilenameFor(name: string, version: string): string { - return `${name.replace(/^@/, "").replace("/", "-")}-${version}.tgz`; -} - -// The slice of a source package's `package.json` this packer reads — -// validated at the read boundary rather than trusted with `as`, since -// a malformed manifest here should fail loud with a parse-time -// message, not surface later as a confusing bundler or tar error. -const PackageManifest = type({ - name: "string", - version: "string", - "exports?": "Record", -}); -type PackageManifest = typeof PackageManifest.infer; - -function entryFileFor(manifest: PackageManifest, packageDir: string): string { - const entry = manifest.exports?.["."]; - if (typeof entry !== "string" || !entry.startsWith("./")) { - throw new Error( - `packToolPackageTarball: ${manifest.name}'s package.json has no "." exports entry to bundle`, - ); - } - return path.join(packageDir, entry); -} - -// Concurrent overlapping seed runs (two racing "finish setup" requests -// for the same tenant, in particular) would otherwise spawn two "bun -// build" subprocesses for the same entrypoint at the same time. The -// packed result for a given `packageDir` is deterministic for this -// process's lifetime — its source only changes across a restart — so -// coalescing concurrent calls into one in-flight build, and reusing a -// completed one, is both correct and strictly less work. -const packCache = new Map>(); - -/** - * Reads `packageDir`'s `package.json` for name/version and its "." - * export as the bundle entrypoint, bundles that entrypoint with the - * `bun build` CLI (no externals — every dependency inlined), and tars - * the result into `package/package.json` + `package/tool.mjs`, - * matching the layout `extractTarballPackageJSON` and npm itself - * expect. - */ -export async function packToolPackageTarball(packageDir: string): Promise { - const cached = packCache.get(packageDir); - if (cached !== undefined) return cached; - const pending = packToolPackageTarballUncached(packageDir).catch((err: unknown) => { - // A failed build must not poison future calls — only a - // successful, reusable result stays cached. - packCache.delete(packageDir); - throw err; - }); - packCache.set(packageDir, pending); - return pending; -} - -// `Bun.build()` called in-process has been observed to fail -// nondeterministically — a bundler-internal "Unexpected reading file" -// on a package this exact process had already loaded fine moments -// earlier — once the calling process has itself transpiled a large -// module graph (this monorepo's own onboarding/connections/e2e code, in -// particular) and other `bun` processes (a spawned hub, a spawned -// sidecar) are alive alongside it. Shelling out to the `bun build` CLI -// as an isolated subprocess sidesteps whatever shared, reentrant -// bundler state the in-process API call was tripping over — the same -// reason `scripts/checks/packages.ts` shells out to `bun pm pack` -// rather than calling a pack API in-process. -async function runBunBuild( - entrypoint: string, - outfile: string, - packageName: string, -): Promise { - const proc = Bun.spawn( - ["bun", "build", entrypoint, "--target=bun", "--format=esm", `--outfile=${outfile}`], - { stdout: "pipe", stderr: "pipe" }, - ); - const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]); - if (exitCode !== 0) { - throw new Error( - `packToolPackageTarball: "bun build" failed for ${packageName} (exit ${String(exitCode)}): ${stderr}`, - ); - } -} - -// The slice of an `AnnotatedToolFactory` export the surface enumeration -// reads, checked by duck type rather than `instanceof` since the loaded -// module crosses a dynamic `import()` boundary — the same shape -// `describe.ts` read before the surface moved into the packed tarball. -type Bundle = { - readonly id: string; - readonly definitions: readonly { - readonly name: string; - readonly approval?: "ask"; - }[]; -}; - -function isBundle(value: unknown): value is Bundle { - return ( - typeof value === "function" && - typeof (value as { id?: unknown }).id === "string" && - Array.isArray((value as { definitions?: unknown }).definitions) - ); -} - -async function surfaceFor(entryFile: string, packageName: string): Promise { - const mod = (await import(entryFile)) as Record; - const surface: ToolSurfaceEntry[] = []; - for (const value of Object.values(mod)) { - if (!isBundle(value)) continue; - for (const definition of value.definitions) { - surface.push({ - qualifiedId: `${value.id}:${definition.name}`, - kind: "tool", - ...(definition.approval !== undefined ? { approval: definition.approval } : {}), - }); - } - } - const manifest = ToolSurfaceManifest({ - name: packageName, - version: "0.0.0", - surface, - }); - if (manifest instanceof type.errors) { - throw new Error( - `packToolPackageTarball: ${packageName}'s tool surface failed validation: ${manifest.summary}`, - ); - } - return manifest.surface; -} - -async function packToolPackageTarballUncached(packageDir: string): Promise { - const manifestJson: unknown = JSON.parse( - await readFile(path.join(packageDir, "package.json"), "utf8"), - ); - const manifest = PackageManifest(manifestJson); - if (manifest instanceof type.errors) { - throw new Error( - `packToolPackageTarball: ${packageDir}'s package.json failed validation: ${manifest.summary}`, - ); - } - - const entryFile = entryFileFor(manifest, packageDir); - const surface = await surfaceFor(entryFile, manifest.name); - - const bundleStagingDir = await mkdtemp(path.join(tmpdir(), "corbits-tools-bundle-")); - let bundleBytes: Uint8Array; - try { - const outfile = path.join(bundleStagingDir, BUNDLE_ENTRY_FILENAME); - await runBunBuild(entryFile, outfile, manifest.name); - bundleBytes = new Uint8Array(await readFile(outfile)); - } finally { - await rm(bundleStagingDir, { recursive: true, force: true }); - } - - const tarballPackageJSON = { - name: manifest.name, - version: manifest.version, - interchange: { tools: `./${BUNDLE_ENTRY_FILENAME}` }, - surface, - }; - - const stagingRoot = await mkdtemp(path.join(tmpdir(), "corbits-tools-pack-")); - try { - const packageStagingDir = path.join(stagingRoot, "package"); - await mkdir(packageStagingDir, { recursive: true }); - await writeFile( - path.join(packageStagingDir, "package.json"), - JSON.stringify(tarballPackageJSON, null, 2), - ); - await writeFile(path.join(packageStagingDir, BUNDLE_ENTRY_FILENAME), bundleBytes); - - const tarballPath = path.join(stagingRoot, "out.tgz"); - await tar.create({ cwd: stagingRoot, gzip: true, file: tarballPath }, ["package"]); - const bytes = new Uint8Array(await readFile(tarballPath)); - return { - name: manifest.name, - version: manifest.version, - filename: tarballFilenameFor(manifest.name, manifest.version), - bytes, - }; - } finally { - await rm(stagingRoot, { recursive: true, force: true }); - } -} diff --git a/packages/tool-registry-publish/src/publish.test.ts b/packages/tool-registry-publish/src/publish.test.ts deleted file mode 100644 index 97f0551b4c..0000000000 --- a/packages/tool-registry-publish/src/publish.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -// The publish flow treats a published name@version as IMMUTABLE: a -// filename that already exists in the registry is skipped (same-source -// rebuilds are not byte-deterministic, so differing bytes cannot prove -// changed code) — shipping a change requires a version bump. -import { describe, expect, test } from "bun:test"; -import { - EmptyRegistryPublishError, - publishCorbitsToolsRegistry, - shouldPublishTarball, - sha512Integrity, - type ApiCall, - type FetchTarballPut, -} from "./publish"; -import { tarballsCoverRequiredSeedPackages } from "./registry"; - -describe("shouldPublishTarball", () => { - test("a brand-new filename publishes", () => { - expect(shouldPublishTarball("corbits-x-tools-0.0.2.tgz", undefined)).toBe(true); - }); - - test("an already-published filename is skipped, whatever its bytes", () => { - const existing = sha512Integrity(new TextEncoder().encode("old bytes")); - expect(shouldPublishTarball("corbits-x-tools-0.0.2.tgz", existing)).toBe(false); - }); -}); - -describe("sha512Integrity", () => { - test("is stable for identical bytes and SRI-shaped", () => { - const bytes = new TextEncoder().encode("same"); - expect(sha512Integrity(bytes)).toBe(sha512Integrity(bytes)); - expect(sha512Integrity(bytes).startsWith("sha512-")).toBe(true); - }); -}); - -describe("tarballsCoverRequiredSeedPackages", () => { - test("an empty listing is not seeded", () => { - expect(tarballsCoverRequiredSeedPackages([])).toBe(false); - }); - - test("covers @corbits/capability-tools at any version", () => { - expect(tarballsCoverRequiredSeedPackages(["corbits-capability-tools-0.0.6.tgz"])).toBe(true); - }); - - test("a dangling registry with unrelated tarballs is not seeded", () => { - expect(tarballsCoverRequiredSeedPackages(["corbits-other-tools-1.0.0.tgz"])).toBe(false); - }); -}); - -describe("publishCorbitsToolsRegistry", () => { - test("success is false when zero packages are uploaded onto an empty registry", async () => { - let createdAssets = 0; - const api: ApiCall = async (method, path) => { - if ( - method === "GET" && - path.includes("kind=package-registry") && - path.includes("inherited=false") - ) { - return { status: 200, data: [], cookies: [] }; - } - if (method === "POST" && path.endsWith("/assets")) { - createdAssets += 1; - return { status: 201, data: {}, cookies: [] }; - } - throw new Error(`unexpected call: ${method} ${path}`); - }; - - try { - await publishCorbitsToolsRegistry({ - api, - cookies: [], - hubUrl: "http://localhost:3000", - tenantId: "ten_1", - checkFreshness: async () => undefined, - packageDirs: [], - }); - throw new Error("expected EmptyRegistryPublishError"); - } catch (cause) { - expect(cause).toBeInstanceOf(EmptyRegistryPublishError); - expect((cause as EmptyRegistryPublishError).success).toBe(false); - expect((cause as EmptyRegistryPublishError).message).toContain("uploaded none"); - expect((cause as EmptyRegistryPublishError).message).toContain( - "still missing @corbits/capability-tools", - ); - } - expect(createdAssets).toBe(0); - }); - - test("success is false when uploads land but capability-tools is still missing", async () => { - const api: ApiCall = async (method, path) => { - if ( - method === "GET" && - path.includes("kind=package-registry") && - path.includes("inherited=false") - ) { - return { status: 200, data: [], cookies: [] }; - } - if (method === "POST" && path.endsWith("/assets")) { - return { - status: 201, - data: { - id: "ast_tools", - tenantId: "ten_1", - kind: "package-registry", - name: "corbits-tools", - displayName: null, - creatorPrincipalId: null, - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", - }, - cookies: [], - }; - } - throw new Error(`unexpected call: ${method} ${path}`); - }; - - try { - await publishCorbitsToolsRegistry({ - api, - cookies: [], - hubUrl: "http://localhost:3000", - tenantId: "ten_1", - checkFreshness: async () => undefined, - packageDirs: ["/tmp/other-tools"], - pack: async () => ({ - name: "@corbits/other-tools", - version: "1.0.0", - filename: "corbits-other-tools-1.0.0.tgz", - bytes: new Uint8Array([1, 2, 3]), - }), - fetchImpl: (async () => - new Response(JSON.stringify({ commit: "abc", integrity: "sha512-x" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - })) satisfies FetchTarballPut, - }); - throw new Error("expected EmptyRegistryPublishError"); - } catch (cause) { - expect(cause).toBeInstanceOf(EmptyRegistryPublishError); - expect((cause as EmptyRegistryPublishError).message).toContain( - "uploaded corbits-other-tools-1.0.0.tgz", - ); - expect((cause as EmptyRegistryPublishError).message).toContain( - "still missing @corbits/capability-tools", - ); - expect((cause as EmptyRegistryPublishError).message).not.toContain("uploaded none"); - } - }); -}); diff --git a/packages/tool-registry-publish/src/publish.ts b/packages/tool-registry-publish/src/publish.ts deleted file mode 100644 index 130663454f..0000000000 --- a/packages/tool-registry-publish/src/publish.ts +++ /dev/null @@ -1,459 +0,0 @@ -// Publishes the packed `@corbits/*-tools` tarballs into a tenant's -// `corbits-tools` package-registry asset over the hub's native REST -// asset routes (find-or-create the asset, then `PUT` each tarball) — -// the only supported write path for a non-hub principal -// (`vendor/intx/hub-sessions/src/package-registry-kind.ts`'s -// `packageRegistryAuthorize`). Idempotent: re-running overwrites -// same-name tarball entries and reuses an existing registry asset. - -import { createHash } from "node:crypto"; -import { AssetResponse, AssetWithOriginResponse } from "@intx/types"; -import { type, type Type } from "arktype"; -import { checkToolPackageFreshness } from "./freshness-check"; -import { materializePackages } from "./materialize"; -import { packToolPackageTarball, type PackedTarball } from "./pack"; -import { - CORBITS_TOOL_PACKAGE_DIRS, - CORBITS_TOOLS_REGISTRY, - REQUIRED_SEED_TOOL_PACKAGES, - tarballCoversPackage, - tarballsCoverRequiredSeedPackages, -} from "./registry"; - -export type ApiResult = { status: number; data: unknown; cookies: string[] }; - -/** - * SRI-shaped integrity of `bytes`, matching `ssri.fromData(bytes, { - * algorithms: ["sha512"] })`'s default single-hash output - * (`vendor/intx/hub-api/src/routes/assets.ts`'s tarball-list route - * computes the same way). Computed locally with `node:crypto` rather - * than adding an `ssri` dependency for one hash. - */ -export function sha512Integrity(bytes: Uint8Array): string { - return `sha512-${createHash("sha512").update(bytes).digest("base64")}`; -} - -/** A `fetch`-shaped call the caller supplies, declared locally so this package never depends on a hub API client package. */ -export type ApiCall = ( - method: string, - path: string, - body?: unknown, - cookies?: string[], -) => Promise; - -const TarballPutResponse = type({ commit: "string", integrity: "string" }); - -export type PublishSummary = { - filename: string; - commit: string; - integrity: string; -}; - -function parseSchema(schema: T, data: unknown, label: string): T["infer"] { - const result = schema(data); - if (result instanceof type.errors) { - throw new Error( - `publishCorbitsToolsRegistry: validation failed for ${label}: ${result.summary}`, - ); - } - return result; -} - -async function listRegistryAsset( - api: ApiCall, - cookies: string[], - tenantId: string, -): Promise { - const listed = await api( - "GET", - `/api/tenants/${tenantId}/assets?kind=package-registry&inherited=false`, - undefined, - cookies, - ); - if (listed.status !== 200) { - throw new Error(`publishCorbitsToolsRegistry: failed to list assets: ${String(listed.status)}`); - } - const rows = parseSchema(AssetWithOriginResponse.array(), listed.data, "list assets response"); - return rows.find((row) => row.name === CORBITS_TOOLS_REGISTRY)?.id; -} - -/** - * Create-first, list-on-conflict — the same ensure-then-create - * tolerance `seedTenant`'s own `ensureWorkflowAsset` uses, so two - * overlapping seed runs for the same tenant never both fail on a - * name collision the substrate itself reports as a 409. - */ -async function ensureRegistryAsset( - api: ApiCall, - cookies: string[], - tenantId: string, -): Promise { - const existing = await listRegistryAsset(api, cookies, tenantId); - if (existing !== undefined) return existing; - - const created = await api( - "POST", - `/api/tenants/${tenantId}/assets`, - { kind: "package-registry", name: CORBITS_TOOLS_REGISTRY }, - cookies, - ); - if (created.status === 201) { - return parseSchema(AssetResponse, created.data, "create asset response").id; - } - // Two overlapping seed runs for the same tenant (the root - // tenant's own boot-time seed racing a second caller seeding that - // same tenant) can both pass the `listRegistryAsset` check above - // before either has created the row, then both post-create — the - // loser hits the `asset_tenant_kind_name` unique constraint. The - // substrate answers that as `409` in the common case; asserted here - // by name too, in case a raw constraint violation surfaces as a - // generic 500 instead, so the loser still resolves the winner's row - // rather than throwing on a race this ensure is meant to tolerate. - const isNameCollision = - created.status === 409 || JSON.stringify(created.data).includes("asset_tenant_kind_name"); - if (!isNameCollision) { - throw new Error( - `publishCorbitsToolsRegistry: failed to create the ${CORBITS_TOOLS_REGISTRY} asset: ${String(created.status)}`, - ); - } - - const afterConflict = await listRegistryAsset(api, cookies, tenantId); - if (afterConflict === undefined) { - throw new Error( - `publishCorbitsToolsRegistry: the ${CORBITS_TOOLS_REGISTRY} asset reported a name conflict but is not listable on the tenant`, - ); - } - return afterConflict; -} - -const TarballSummary = type({ - filename: "string", - size: "number", - integrity: "string", -}); -const TarballListResponse = TarballSummary.array(); - -/** - * Thrown when a publish run leaves `corbits-tools` without the required - * seed tarballs (`@corbits/capability-tools`). Covers both zero uploads onto - * an empty/dangling registry (`GET tarballs` → `[]`) and a partial run - * that uploaded other packages but still cannot resolve first-launch - * pins. Callers must not treat this as success. - */ -export class EmptyRegistryPublishError extends Error { - readonly success = false as const; - constructor(detail: { uploaded: readonly string[]; missing: readonly string[] }) { - const uploadedBit = - detail.uploaded.length === 0 ? "uploaded none" : `uploaded ${detail.uploaded.join(", ")}`; - super( - `publishCorbitsToolsRegistry: the ${CORBITS_TOOLS_REGISTRY} registry is missing the expected tool-package tarballs (${uploadedBit}; still missing ${detail.missing.join(", ")})`, - ); - this.name = "EmptyRegistryPublishError"; - } -} - -/** - * Guard against publishing a tarball whose filename (hence its - * `name@version`) already exists in the registry with different - * bytes. Same filename + same content is a harmless no-op re-publish - * (a retried CI run, a rebuild that reproduced byte-identical output); - * same filename + different content means the caller changed the - * package's source without bumping its version, which would silently - * strand every consumer of `name@version` on the old bytes. Exported - * standalone so the check is unit-testable without invoking a real - * `bun build` through `packToolPackageTarball`. - */ -/** A published name@version is immutable: once a tarball exists under a - * filename, later publishes SKIP it (returning false) rather than - * overwrite — same-source rebuilds are not byte-deterministic (tar - * mtimes), so differing bytes cannot prove changed code. Shipping a - * code change requires a version bump; the skip is logged loudly by the - * caller so a forgotten bump is visible, never silent. */ -export function shouldPublishTarball( - _filename: string, - existingIntegrity: string | undefined, -): boolean { - return existingIntegrity === undefined; -} - -async function listExistingTarballs( - api: ApiCall, - cookies: string[], - tenantId: string, - assetId: string, -): Promise> { - const listed = await api( - "GET", - `/api/tenants/${tenantId}/assets/${assetId}/tarballs`, - undefined, - cookies, - ); - if (listed.status !== 200) { - throw new Error( - `publishCorbitsToolsRegistry: failed to list existing tarballs: ${String(listed.status)}`, - ); - } - const rows = parseSchema(TarballListResponse, listed.data, "list tarballs response"); - return new Map(rows.map((row) => [row.filename, row.integrity])); -} - -/** - * Whether this tenant can already resolve the seeded `@corbits/*-tools` - * tarballs — locally or inherited. Read-only: it never creates an - * asset. An existing `corbits-tools` row with `GET tarballs` → `[]` is - * not seeded (the dangling git-init-no-commit case). - */ -export async function isCorbitsToolsRegistrySeeded( - api: ApiCall, - cookies: string[], - tenantId: string, -): Promise { - const listed = await api( - "GET", - `/api/tenants/${tenantId}/assets?kind=package-registry&inherited=true`, - undefined, - cookies, - ); - if (listed.status !== 200) { - throw new Error(`publishCorbitsToolsRegistry: failed to list assets: ${String(listed.status)}`); - } - const rows = parseSchema(AssetWithOriginResponse.array(), listed.data, "list assets response"); - const local = rows.find((row) => row.name === CORBITS_TOOLS_REGISTRY && row.origin.direct); - const inherited = rows.find((row) => row.name === CORBITS_TOOLS_REGISTRY); - const asset = local ?? inherited; - if (asset === undefined) return false; - - const tarballs = await api( - "GET", - `/api/tenants/${tenantId}/assets/${asset.id}/tarballs`, - undefined, - cookies, - ); - if (tarballs.status !== 200) return false; - const tarballRows = parseSchema(TarballListResponse, tarballs.data, "list tarballs response"); - return tarballsCoverRequiredSeedPackages(tarballRows.map((row) => row.filename)); -} - -export type FetchTarballPut = (input: string, init: RequestInit) => Promise; - -async function putTarball( - hubUrl: string, - cookies: string[], - tenantId: string, - assetId: string, - tarball: { filename: string; bytes: Uint8Array }, - fetchImpl: FetchTarballPut, -): Promise { - const headers: Record = { - "Content-Type": "application/octet-stream", - }; - if (cookies.length > 0) headers["Cookie"] = cookies.join("; "); - const response = await fetchImpl( - `${hubUrl}/api/tenants/${tenantId}/assets/${assetId}/tarballs/${tarball.filename}`, - { - method: "PUT", - headers, - body: Uint8Array.from(tarball.bytes), - redirect: "manual", - }, - ); - if (response.status !== 200) { - const text = await response.text(); - throw new Error( - `publishCorbitsToolsRegistry: upload failed for ${tarball.filename}: ${String(response.status)} ${text}`, - ); - } - const parsed = parseSchema(TarballPutResponse, await response.json(), `PUT ${tarball.filename}`); - return { filename: tarball.filename, ...parsed }; -} - -export type PublishCorbitsToolsRegistryArgs = { - api: ApiCall; - cookies: string[]; - hubUrl: string; - tenantId: string; - fetchImpl?: FetchTarballPut; - log?: (line: string) => void; - /** Test seam. Production omits this and runs `checkToolPackageFreshness`. */ - checkFreshness?: () => Promise; - /** Test seam. Production omits this and packs `CORBITS_TOOL_PACKAGE_DIRS`. */ - packageDirs?: readonly string[]; - /** Test seam. Production omits this and uses `packToolPackageTarball`. */ - pack?: (packageDir: string) => Promise; -}; - -export type FetchTarballSource = (url: string, init?: RequestInit) => Promise; - -/** - * Fetches a prebuilt tarball from a `tarball-url` pin's URL and verifies - * its bytes against the pin's SRI-shaped `sha512-…` integrity — the same - * hash shape `sha512Integrity` computes and the hub's tarball routes - * store. A mismatched fetch must never be published: the registry's - * name@version immutability means bad bytes, once in, stay in. - */ -export async function fetchRegistryTarballSource(args: { - url: string; - integrity: string; - fetchImpl?: FetchTarballSource; -}): Promise { - const fetchImpl = args.fetchImpl ?? fetch; - const response = await fetchImpl(args.url, { redirect: "follow" }); - if (!response.ok) { - throw new Error(`fetchRegistryTarballSource: ${args.url} responded ${String(response.status)}`); - } - const bytes = new Uint8Array(await response.arrayBuffer()); - const actual = sha512Integrity(bytes); - if (actual !== args.integrity) { - throw new Error( - `fetchRegistryTarballSource: integrity mismatch for ${args.url} (expected ${args.integrity}, got ${actual})`, - ); - } - return bytes; -} - -/** - * Installs one externally-sourced tarball into the tenant's - * `corbits-tools` registry through the same native PUT - * `publishCorbitsToolsRegistry` uses: ensure the registry asset, skip an - * already-published filename (name@version is immutable — a rebuild is - * not byte-deterministic, so re-PUTs are never attempted), fetch the - * bytes through `fetchSource`, verify, and upload. This is the - * tarball-url half of the desired-state reconcile's tool install; it is - * deliberately NOT wired into any pin set yet — no external artifacts - * exist — but ships tested so populating a pin is a data edit. - */ -export async function installRegistryTarball(args: { - api: ApiCall; - cookies: string[]; - hubUrl: string; - tenantId: string; - name: string; - version: string; - fetchSource: () => Promise; - fetchImpl?: FetchTarballPut; - log?: (line: string) => void; -}): Promise<"installed" | "present"> { - const filename = `${args.name.replace(/^@/, "").replace("/", "-")}-${args.version}.tgz`; - const assetId = await ensureRegistryAsset(args.api, args.cookies, args.tenantId); - const existing = await listExistingTarballs(args.api, args.cookies, args.tenantId, assetId); - if (!shouldPublishTarball(filename, existing.get(filename))) { - args.log?.(`${filename} already published (skipped)`); - return "present"; - } - const bytes = await args.fetchSource(); - await putTarball( - args.hubUrl, - args.cookies, - args.tenantId, - assetId, - { filename, bytes }, - args.fetchImpl ?? fetch, - ); - args.log?.(`published ${filename} from an external tarball source`); - return "installed"; -} - -function missingRequiredSeedPackages(filenames: readonly string[]): string[] { - return REQUIRED_SEED_TOOL_PACKAGES.filter( - (name) => !filenames.some((filename) => tarballCoversPackage(filename, name)), - ); -} - -export type PublishCorbitsToolsRegistryResult = { - success: boolean; - summaries: PublishSummary[]; -}; - -/** - * Ensures the tenant's `corbits-tools` package-registry asset exists - * and carries every package in `CORBITS_TOOL_PACKAGE_DIRS`, packing - * and pushing whatever is missing. Packs before creating the asset so - * a failed pack cannot leave a dangling git-init-no-commit row. - * Zero uploads onto a registry that still lacks the required tarballs - * throw `EmptyRegistryPublishError` (`success === false`) instead of - * reporting success. - */ -export async function publishCorbitsToolsRegistry( - args: PublishCorbitsToolsRegistryArgs, -): Promise { - const fetchImpl = args.fetchImpl ?? fetch; - const log = args.log ?? ((): void => undefined); - const checkFreshness = args.checkFreshness ?? checkToolPackageFreshness; - const packageDirs = args.packageDirs ?? CORBITS_TOOL_PACKAGE_DIRS; - const pack = args.pack ?? packToolPackageTarball; - - // Fail before packing if any `@corbits/*-tools` src/ moved without a - // version bump. Publish skips an already-present filename, so a forgotten - // bump would otherwise ship nothing and leave running agents on old tools. - await checkFreshness(); - - const existingId = await listRegistryAsset(args.api, args.cookies, args.tenantId); - const existingIntegrityByFilename = - existingId !== undefined - ? await listExistingTarballs(args.api, args.cookies, args.tenantId, existingId) - : new Map(); - - // Packs are independent; PUTs stay serial because they mutate one - // package-registry git asset. Pack before ensure-create so a pack - // failure cannot leave a dangling empty registry row. - const packed = await materializePackages({ - packageDirs, - materialize: pack, - }); - - const toUpload: PackedTarball[] = []; - for (const item of packed) { - if (item.status !== "ok") continue; - const tarball = item.value; - if ( - !shouldPublishTarball(tarball.filename, existingIntegrityByFilename.get(tarball.filename)) - ) { - args.log?.( - `${tarball.filename} already published — versions are immutable; bump the version to ship changes (skipped)`, - ); - continue; - } - toUpload.push(tarball); - } - - const existingFilenames = [...existingIntegrityByFilename.keys()]; - if (toUpload.length === 0) { - if (!tarballsCoverRequiredSeedPackages(existingFilenames)) { - throw new EmptyRegistryPublishError({ - uploaded: [], - missing: missingRequiredSeedPackages(existingFilenames), - }); - } - return { success: true, summaries: [] }; - } - - const assetId = existingId ?? (await ensureRegistryAsset(args.api, args.cookies, args.tenantId)); - - const summaries: PublishSummary[] = []; - for (const tarball of toUpload) { - const summary = await putTarball( - args.hubUrl, - args.cookies, - args.tenantId, - assetId, - tarball, - fetchImpl, - ); - log( - `published ${tarball.name}@${tarball.version} to ${CORBITS_TOOLS_REGISTRY} (commit=${summary.commit})`, - ); - summaries.push(summary); - } - - const publishedFilenames = [ - ...existingFilenames, - ...summaries.map((summary) => summary.filename), - ]; - if (!tarballsCoverRequiredSeedPackages(publishedFilenames)) { - throw new EmptyRegistryPublishError({ - uploaded: summaries.map((summary) => summary.filename), - missing: missingRequiredSeedPackages(publishedFilenames), - }); - } - return { success: true, summaries }; -} diff --git a/packages/tool-registry-publish/src/registry.ts b/packages/tool-registry-publish/src/registry.ts deleted file mode 100644 index 7e1d01d4c9..0000000000 --- a/packages/tool-registry-publish/src/registry.ts +++ /dev/null @@ -1,66 +0,0 @@ -// The one site naming the tenant-scoped package-registry asset that -// resolves `@corbits/*` tool-package pins, and the packages published -// into it. A rename or an added package changes one file instead of -// chasing string literals across `apps/hub` (scope routing) and every -// publish call site — the same reasoning `WORKSPACE_BUILTINS_REGISTRY` -// documents in vendor/intx/hub-sessions/src/package-registry-kind.ts. - -/** - * Asset name the hub's `@corbits` scope routing resolves tool-package - * pins against (`apps/hub/src/index.ts`'s `CORBITS_TOOLS_REGISTRY`). - * Kept here so the publisher and the hub's scope-routing config read - * the same literal. - */ -export const CORBITS_TOOLS_REGISTRY = "corbits-tools"; - -/** - * Package names a seeded `corbits-tools` registry must carry for the - * default workflow set to launch. Default workflows pin at least these - * (Myra's assistant pins `@corbits/capability-tools` into every drafted - * agent's default tool-package set); an empty or dangling registry — a - * package-registry row whose git repo has no tarball commits — is not - * seeded. `@corbits/memory` is not listed here: it is a - * published dependency this repo does not build or publish into this - * registry itself, not one of the workspace `packages/*-tools` this - * publisher packs. - */ -export const REQUIRED_SEED_TOOL_PACKAGES = ["@corbits/capability-tools"] as const; - -/** True when `filename` is an npm-style tarball for `packageName` (any version). */ -export function tarballCoversPackage(filename: string, packageName: string): boolean { - const prefix = `${packageName.replace(/^@/, "").replace("/", "-")}-`; - return filename.startsWith(prefix) && filename.endsWith(".tgz"); -} - -/** - * Whether a tarball listing is enough for first-run launch: non-empty - * and covering every `REQUIRED_SEED_TOOL_PACKAGES` entry. An empty list - * is the dangling-asset case (`GET tarballs` → `[]` after git init with - * no commit). - */ -export function tarballsCoverRequiredSeedPackages(filenames: Iterable): boolean { - const list = [...filenames]; - if (list.length === 0) return false; - return REQUIRED_SEED_TOOL_PACKAGES.every((name) => - list.some((filename) => tarballCoversPackage(filename, name)), - ); -} - -/** - * Absolute directories of the `@corbits/*-tools` packages published - * into the `corbits-tools` registry. Every workflow's - * `toolPackagePins` under the `@corbits` scope must name a package - * listed here, or its pin never resolves. `capability-tools` - * is published here and pinned into every drafted - * agent's default tool-package set — see `@corbits/capability-tools`'s - * README for how its request_capability tool reaches the hub. - */ -export const CORBITS_TOOL_PACKAGE_DIRS: readonly string[] = [ - new URL("../../../tools/capability", import.meta.url).pathname, - new URL("../../../tools/catalog", import.meta.url).pathname, - new URL("../../../tools/agent-directory", import.meta.url).pathname, - new URL("../../../tools/access", import.meta.url).pathname, - new URL("../../../tools/interaction", import.meta.url).pathname, - new URL("../../../tools/skills", import.meta.url).pathname, - new URL("../../../tools/workflow-authoring", import.meta.url).pathname, -]; diff --git a/packages/tool-registry-publish/src/surface-reader.test.ts b/packages/tool-registry-publish/src/surface-reader.test.ts deleted file mode 100644 index 7ea4d66a1d..0000000000 --- a/packages/tool-registry-publish/src/surface-reader.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import * as tar from "tar"; -import { packToolPackageTarball } from "./pack"; -import { readToolSurfaceManifests } from "./surface-reader"; - -/** A registry tarball published before manifests existed: a real tgz - * whose package.json parses but carries no `surface` key. */ -async function legacyTarball(): Promise { - const dir = await mkdtemp(path.join(tmpdir(), "legacy-pkg-")); - await mkdir(path.join(dir, "package"), { recursive: true }); - await writeFile( - path.join(dir, "package", "package.json"), - JSON.stringify({ - name: "@corbits/pre-manifest-legacy", - version: "1.0.0", - interchange: { tools: {} }, - }), - ); - const out = path.join(dir, "legacy.tgz"); - await tar.create({ cwd: dir, file: out, gzip: true }, ["package"]); - return new Uint8Array(await readFile(out)); -} - -function memorySource(files: Map) { - return { - listBlobs: (dir: string) => - Promise.resolve([...files.keys()].filter((path) => path.startsWith(`${dir}/`))), - readBlob: (path: string) => { - const bytes = files.get(path); - if (bytes === undefined) return Promise.reject(new Error(`no ${path}`)); - return Promise.resolve(bytes); - }, - }; -} - -describe("readToolSurfaceManifests", () => { - test("round-trips a packed tarball into a validated manifest", async () => { - const tarball = await packToolPackageTarball( - new URL("../../../tools/capability", import.meta.url).pathname, - ); - const source = memorySource(new Map([[`tarballs/${tarball.filename}`, tarball.bytes]])); - const manifests = await readToolSurfaceManifests({ - ...source, - rootDir: "tarballs", - }); - expect(manifests).toHaveLength(1); - expect(manifests[0]?.name).toBe("@corbits/capability-tools"); - expect(manifests[0]?.surface.map((entry) => entry.qualifiedId)).toContain( - "@corbits/capability-tools/cap:request_capability", - ); - expect(manifests[0]?.surface.every((entry) => entry.kind === "tool")).toBe(true); - }); - - test("ignores non-tarball blobs and reports none when the registry is empty", async () => { - const source = memorySource(new Map([["tarballs/README.md", new TextEncoder().encode("hi")]])); - expect(await readToolSurfaceManifests({ ...source, rootDir: "tarballs" })).toEqual([]); - }); - - test("skips a tarball whose package.json is not a valid manifest", async () => { - const tarball = await packToolPackageTarball( - new URL("../../../tools/capability", import.meta.url).pathname, - ); - // Corrupt the packaged manifest by re-packing a tampered file is - // overkill; a truncated tarball is enough to prove the reader skips - // the blob instead of failing the whole read. Registries published - // before manifests existed hold such blobs, and shouldPublishTarball - // never re-uploads an existing name@version to heal them. - const truncated = tarball.bytes.slice(0, 64); - const source = memorySource(new Map([["tarballs/broken.tgz", truncated]])); - expect(await readToolSurfaceManifests({ ...source, rootDir: "tarballs" })).toEqual([]); - }); - - test("returns the valid manifests and skips the manifest-less legacy ones", async () => { - const tarball = await packToolPackageTarball( - new URL("../../../tools/capability", import.meta.url).pathname, - ); - const source = memorySource( - new Map([ - [`tarballs/${tarball.filename}`, tarball.bytes], - ["tarballs/legacy.tgz", await legacyTarball()], - ]), - ); - const manifests = await readToolSurfaceManifests({ - ...source, - rootDir: "tarballs", - }); - expect(manifests).toHaveLength(1); - expect(manifests[0]?.name).toBe("@corbits/capability-tools"); - }); -}); diff --git a/packages/tool-registry-publish/src/surface-reader.ts b/packages/tool-registry-publish/src/surface-reader.ts deleted file mode 100644 index 851abe8503..0000000000 --- a/packages/tool-registry-publish/src/surface-reader.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Reads every packed tarball's tool-surface manifest back out of a -// `corbits-tools` package-registry asset's blob tree — the hub-side -// replacement for importing each package's source (`describe.ts`, now -// gone). Blob access is injected so the hub wires it to its launch-path -// `assetService` wrapper (`apps/hub/src/launch-caches.ts`'s SHA-keyed -// `readAssetBlob`), keeping this module free of hub or vendor imports. -import { getLogger } from "@intx/log"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import * as tar from "tar"; -import { type } from "arktype"; -import { ToolSurfaceManifest } from "./manifest"; - -const log = getLogger(["tool-registry-publish", "surface-reader"]); - -export type ToolSurfaceBlobSource = { - /** Lists blob paths under `dir` (repo-root-relative, like `listAssetBlobs`). */ - listBlobs: (dir: string) => Promise; - /** Reads one blob's bytes (repo-root-relative path, like `readAssetBlob`). */ - readBlob: (path: string) => Promise; - /** The asset directory the packed tarballs live under. */ - rootDir: string; -}; - -/** - * Opens each `tarballs/*.tgz` blob, extracts its `package/package.json`, - * and parses it with `ToolSurfaceManifest`. A tarball that carries no - * valid manifest is skipped with a log line, not a thrown error: - * registries published before manifests existed still hold such - * tarballs, and `shouldPublishTarball` never re-uploads an existing - * `name@version` — so one unreadable blob must not take down every - * grants read on a registry that can no longer heal itself. - */ -export async function readToolSurfaceManifests( - source: ToolSurfaceBlobSource, -): Promise { - const blobs = await source.listBlobs(source.rootDir); - const tarballPaths = blobs.filter( - (blob) => blob.startsWith(`${source.rootDir}/`) && blob.endsWith(".tgz"), - ); - const manifests: ToolSurfaceManifest[] = []; - for (const blobPath of tarballPaths) { - const manifest = await manifestFromBlob(blobPath, source); - if (manifest !== undefined) manifests.push(manifest); - } - return manifests; -} - -async function manifestFromBlob( - blobPath: string, - source: ToolSurfaceBlobSource, -): Promise { - let packageJson: unknown; - try { - const bytes = await source.readBlob(blobPath); - packageJson = await extractTarballPackageJSON(bytes); - } catch (error) { - // report-error-ignore: an unreadable tarball in a registry is expected - // legacy data, not an incident — registries published before manifests - // carry such blobs and shouldPublishTarball can never heal them; a log - // line per skip is the whole response. - log.warn`skipping ${blobPath}: unreadable tarball (${error instanceof Error ? error.message : String(error)})`; - return undefined; - } - const manifest = ToolSurfaceManifest(packageJson); - if (manifest instanceof type.errors) { - log.warn`skipping ${blobPath}: package.json is not a valid tool-surface manifest (${manifest.summary})`; - return undefined; - } - return manifest; -} - -async function extractTarballPackageJSON(bytes: Uint8Array): Promise { - const extractDir = await mkdtemp(path.join(tmpdir(), "corbits-surface-")); - try { - const tarballPath = path.join(extractDir, "in.tgz"); - await writeFile(tarballPath, bytes); - await tar.extract({ cwd: extractDir, file: tarballPath }); - return JSON.parse( - await readFile(path.join(extractDir, "package", "package.json"), "utf8"), - ) as unknown; - } finally { - await rm(extractDir, { recursive: true, force: true }); - } -} diff --git a/packages/tool-registry-publish/tsconfig.json b/packages/tool-registry-publish/tsconfig.json deleted file mode 100644 index bee7b64f31..0000000000 --- a/packages/tool-registry-publish/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src"], - "exclude": [], - "references": [ - { - "path": "../../vendor/intx/inference/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/types/tsconfig.src.json" - } - ] -} diff --git a/packages/tool-registry-publish/tsconfig.src.json b/packages/tool-registry-publish/tsconfig.src.json deleted file mode 100644 index 23572def72..0000000000 --- a/packages/tool-registry-publish/tsconfig.src.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - }, - "references": [ - { - "path": "../../vendor/intx/inference/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/types/tsconfig.src.json" - } - ] -} diff --git a/tools/access/package.json b/tools/access/package.json index a7e32b79b4..6f0d439e85 100644 --- a/tools/access/package.json +++ b/tools/access/package.json @@ -30,7 +30,6 @@ "devDependencies": { "@corbits/agent-directory": "workspace:*", "@corbits/agent-directory-tools": "workspace:*", - "@corbits/tool-registry-publish": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:" }, diff --git a/tools/workflow-authoring/src/tool.ts b/tools/workflow-authoring/src/tool.ts index fc2b0c902e..967ed78393 100644 --- a/tools/workflow-authoring/src/tool.ts +++ b/tools/workflow-authoring/src/tool.ts @@ -181,7 +181,7 @@ async function runDeploy(env: WorkflowAuthoringEnv, call: ToolCall): Promise:workflow_source_read` * cannot fit OpenAI's 64-character cap. `defineTool` only requires the - * `@scope/pkg/name` shape; `packages/tool-registry-publish`'s + * `@scope/pkg/name` shape; `apps/web/src/tools/registry-publish.test.ts`'s * tool-name-limits test is what this id satisfies. */ export const workflowAuthoringTools = defineTool({ diff --git a/tsconfig.build.json b/tsconfig.build.json index d5069227ba..fc244fecb4 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -7,15 +7,9 @@ { "path": "./apps/sidecar/tsconfig.src.json" }, - { - "path": "./packages/agent-events/tsconfig.src.json" - }, { "path": "./packages/approvals/tsconfig.src.json" }, - { - "path": "./packages/artifact-ui/tsconfig.src.json" - }, { "path": "./packages/collections/tsconfig.src.json" }, @@ -28,9 +22,6 @@ { "path": "./packages/inference-catalog/tsconfig.src.json" }, - { - "path": "./packages/tool-registry-publish/tsconfig.src.json" - }, { "path": "./packages/url-path/tsconfig.src.json" },