diff --git a/apps/hub/src/hub-error-handler.test.ts b/apps/hub/src/hub-error-handler.test.ts index 49e3a24601..2b0d250c0d 100644 --- a/apps/hub/src/hub-error-handler.test.ts +++ b/apps/hub/src/hub-error-handler.test.ts @@ -6,10 +6,31 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { Hono } from "hono"; import { configureSync, resetSync } from "@intx/log"; -import { InferenceResolutionError } from "@corbits/chat"; import { hubErrorHandler } from "./hub-error-handler"; +/** + * `hubErrorHandler` maps a guidance-bearing error by its string `.name` + * (duck-typed, see `hub-error-handler.ts`), never by `instanceof` — so this + * stands in for the real `InferenceResolutionError` (`@corbits/chat`'s + * `model-unavailable.ts`) without importing it. + */ +const MODEL_UNAVAILABLE_CONSUMER_MESSAGE = + "This agent's model isn't available here."; + +class FakeInferenceResolutionError extends Error { + readonly guidance: string; + constructor(launchLabel: string, resolutionMessage: string) { + super( + `cannot resolve an inference source for ${launchLabel} ` + + `(${resolutionMessage}); seed a tenant catalog source (provider, ` + + `credential, catalog model/provider/offering) before launching`, + ); + this.name = "InferenceResolutionError"; + this.guidance = MODEL_UNAVAILABLE_CONSUMER_MESSAGE; + } +} + let records: { properties: Record }[]; function installCapturingSink(): void { @@ -93,7 +114,7 @@ describe("hubErrorHandler", () => { const app = new Hono(); app.onError(hubErrorHandler()); app.get("/wake", () => { - throw new InferenceResolutionError( + throw new FakeInferenceResolutionError( "the woken instance", 'No launchable inference source for model "claude-sonnet-5"', ); diff --git a/apps/hub/src/mail-redelivery.ts b/apps/hub/src/mail-redelivery.ts index fc9d838b57..ece570eef9 100644 --- a/apps/hub/src/mail-redelivery.ts +++ b/apps/hub/src/mail-redelivery.ts @@ -7,9 +7,9 @@ // notification — so, unlike a payload naming only the recipients, this // one genuinely supports a retry rather than only a log line. // -// `ensureAwake` is chat's own wake primitive (`@corbits/chat`'s -// `ChatPlatform` has no generic host-agnostic equivalent across chat, -// tasks, and any other future resident kind). A recipient it rejects +// `ensureAwake` is the chat platform's own wake primitive (it has no +// generic host-agnostic equivalent across chat, tasks, and any other +// future resident kind). A recipient it rejects // for — not a chat resident at all, or a task-launched resident whose // `wake` is deliberately unreachable once its one-shot turn has run — // still gets the `routeMail` retry, since an address can go from diff --git a/apps/hub/src/mcp-credential-bindings.ts b/apps/hub/src/mcp-credential-bindings.ts index d425eb1c8f..57bf582f97 100644 --- a/apps/hub/src/mcp-credential-bindings.ts +++ b/apps/hub/src/mcp-credential-bindings.ts @@ -1,9 +1,6 @@ -// The `McpCredentialBindingsFor` port `createHubChatPlatform`'s -// `CreateHubChatPlatformDeps` is wired with — see `@corbits/chat`'s -// `platform-adapter.ts` for why this has to be supplied by the -// composition root rather than derived by the deploy-time -// capability walk. `@corbits/mcp-tools`' credential handles are dynamic -// (one `mcp.` per tenant-connected server), so this builds one +// The composition root's own port supplying dynamic MCP credential +// bindings. `@corbits/mcp-tools`' credential handles are dynamic (one +// `mcp.` per tenant-connected server), so this builds one // `CredentialBinding` per connection `@corbits/connections`' own // `listMcpServerConnections` lists for the tenant: `handle` mirrors the // `mcp.` convention `@corbits/mcp-tools`' `mcpCredentialHandle(slug)` @@ -11,11 +8,19 @@ // grammar), while `provider` names the stored `mcp:` provider row // `listMcpServerConnections` read it from — a separate, unconstrained // namespace. +// +// `McpCredentialBindingsFor`'s shape is declared locally rather than +// imported: it names nothing chat-specific (`CredentialBinding` is native +// `@intx/types`), so the hub owns its own copy of the port instead of +// depending on `@corbits/chat` for it. import type { DB } from "@intx/db"; import type { CredentialBinding } from "@intx/types"; -import type { McpCredentialBindingsFor } from "@corbits/chat"; import { listMcpServerConnections } from "@corbits/connections"; +export type McpCredentialBindingsFor = ( + tenantId: string, +) => Promise; + const MCP_TOOLS_PACKAGE = "@corbits/mcp-tools"; export function createMcpCredentialBindingsFor( diff --git a/apps/hub/src/pinned-package-credential-bindings.ts b/apps/hub/src/pinned-package-credential-bindings.ts index eff436b0b3..0afe7bfeb3 100644 --- a/apps/hub/src/pinned-package-credential-bindings.ts +++ b/apps/hub/src/pinned-package-credential-bindings.ts @@ -1,21 +1,25 @@ -// The `PinnedPackageCredentialBindingsFor` port `createHubChatPlatform`'s -// `CreateHubChatPlatformDeps` is wired with — see `@corbits/chat`'s -// `platform-adapter.ts` for why this has to be supplied by the -// composition root rather than declared as -// a required assistant `credentialBindings` entry. Static-handle packages -// (`@corbits/manus-tools`, granola-tools, …) declare -// `interchange.credentials`, but requiring those binds on the assistant -// would throw `MissingCredentialError` on signup / first chat. This -// factory emits a tenant binding only when a pin names a package a +// The composition root's own port for a provisioned agent's pinned tool +// packages: a tenant binding only when a pin names a package a // `CONNECTOR_REGISTRY` entry `feedsTools` AND the tenant already has a // connected credential for that connector (`isConnectorConnected`, the -// same owning check `createWorkflowConnectionRoutes` uses — not -// `@corbits/chat`'s catalog-only `listConnectedProviders`). +// same owning check `createWorkflowConnectionRoutes` uses). Static-handle +// packages (`@corbits/manus-tools`, granola-tools, …) declare +// `interchange.credentials`, but requiring those binds unconditionally +// would throw `MissingCredentialError` on signup / first launch. +// +// `PinnedPackageCredentialBindingsFor`'s shape is declared locally rather +// than imported: it names nothing chat-specific (`CredentialBinding`, +// `ToolPackagePin` are both native `@intx/types`), so the hub owns its own +// copy of the port instead of depending on `@corbits/chat` for it. import type { CredentialBinding } from "@intx/types"; import type { ToolPackagePin } from "@intx/types/tool-packages"; -import type { PinnedPackageCredentialBindingsFor } from "@corbits/chat"; import { CONNECTOR_REGISTRY } from "./native-connector-registry"; +export type PinnedPackageCredentialBindingsFor = ( + tenantId: string, + pins: readonly ToolPackagePin[], +) => Promise; + export type IsConnectorConnected = ( tenantId: string, connectorId: string, diff --git a/apps/hub/src/templates/template-block-routes.ts b/apps/hub/src/templates/template-block-routes.ts index e444f0d0ef..ca32c3d726 100644 --- a/apps/hub/src/templates/template-block-routes.ts +++ b/apps/hub/src/templates/template-block-routes.ts @@ -27,9 +27,8 @@ export type TemplateBlockRoutesDeps = { * `userMessage`, the raw detail lands here. */ log: (line: string) => void; /** The tenant's default inference preferences — the same resolution a - * fresh chat host launches against (`@corbits/chat`'s - * `createWorkbenchHostInferencePreferencesResolver`), so a deployed - * block runs on the model the bench actually connected. */ + * fresh agent launch resolves against, so a deployed block runs on the + * model the bench actually connected. */ inferencePreferences( tenantId: string, ): Promise; diff --git a/apps/hub/src/tool-grants.ts b/apps/hub/src/tool-grants.ts index 25de72cf61..c6ecb1cd81 100644 --- a/apps/hub/src/tool-grants.ts +++ b/apps/hub/src/tool-grants.ts @@ -9,10 +9,6 @@ // `ask` for a tool the package itself marks `approval: "ask"` — the // grants come from the installed asset's own manifest, never from a // source import. -import type { - PinnedToolGrantDeclaration, - ToolGrantsForPins, -} from "@corbits/chat"; import { CORBITS_TOOLS_REGISTRY, readToolSurfaceManifests, @@ -20,6 +16,24 @@ import { } from "@corbits/tool-registry-publish"; import type { AssetWithOrigin } from "@intx/db"; import type { AssetService } from "@intx/hub-sessions"; +import type { GrantEffect } from "@intx/types"; +import type { ToolPackagePin } from "@intx/types/tool-packages"; + +/** + * Declared locally rather than imported from `@corbits/chat`: nothing + * about a pinned tool package's grants is chat-specific, and the shape + * names only native `@intx/types` (`GrantEffect`, `ToolPackagePin`). + */ +export type PinnedToolGrantDeclaration = { + readonly resource: string; + readonly action: "invoke"; + readonly effect: GrantEffect; +}; + +export type ToolGrantsForPins = ( + tenantId: string, + pins: readonly ToolPackagePin[], +) => Promise; export type CreateToolGrantsForPinsDeps = { /**