Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions apps/hub/src/hub-error-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> }[];

function installCapturingSink(): void {
Expand Down Expand Up @@ -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"',
);
Expand Down
6 changes: 3 additions & 3 deletions apps/hub/src/mail-redelivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 12 additions & 7 deletions apps/hub/src/mcp-credential-bindings.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
// 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.<slug>` 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.<slug>` per tenant-connected server), so this builds one
// `CredentialBinding` per connection `@corbits/connections`' own
// `listMcpServerConnections` lists for the tenant: `handle` mirrors the
// `mcp.<slug>` convention `@corbits/mcp-tools`' `mcpCredentialHandle(slug)`
// resolves against (conforms to `@intx/types`' `ToolCredentialHandle`
// grammar), while `provider` names the stored `mcp:<slug>` 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<readonly CredentialBinding[]>;

const MCP_TOOLS_PACKAGE = "@corbits/mcp-tools";

export function createMcpCredentialBindingsFor(
Expand Down
28 changes: 16 additions & 12 deletions apps/hub/src/pinned-package-credential-bindings.ts
Original file line number Diff line number Diff line change
@@ -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<readonly CredentialBinding[]>;

export type IsConnectorConnected = (
tenantId: string,
connectorId: string,
Expand Down
5 changes: 2 additions & 3 deletions apps/hub/src/templates/template-block-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BlockWorkflowBuildInput["inferencePreferences"]>;
Expand Down
22 changes: 18 additions & 4 deletions apps/hub/src/tool-grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,31 @@
// `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,
type ToolSurfaceBlobSource,
} 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<readonly PinnedToolGrantDeclaration[]>;

export type CreateToolGrantsForPinsDeps = {
/**
Expand Down
Loading