diff --git a/packages/capability-tools/package.json b/packages/capability-tools/package.json index 4b4dbb4cd7..42bdeee057 100644 --- a/packages/capability-tools/package.json +++ b/packages/capability-tools/package.json @@ -2,7 +2,7 @@ "name": "@corbits/capability-tools", "private": true, "description": "The request_capability tool as an @intx/agent tool bundle (CL-6084): an agent asks in-chat for a tool package, skill, or model it doesn't have, gated behind Interchange's native per-invocation approval, and \u2014 once approved \u2014 calls the sanctioned workflow-run capabilities surface with the run's own bearer token, never a model-supplied identity", - "version": "0.0.5", + "version": "0.0.6", "license": "LGPL-2.1-or-later", "type": "module", "exports": { @@ -13,7 +13,6 @@ "test": "bun test" }, "dependencies": { - "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/types": "workspace:*", "arktype": "catalog:" diff --git a/packages/capability-tools/src/client.ts b/packages/capability-tools/src/client.ts index dc8cf3bdd0..7b00092191 100644 --- a/packages/capability-tools/src/client.ts +++ b/packages/capability-tools/src/client.ts @@ -27,7 +27,7 @@ import { runBearerErrorMessage, runBearerFetch, type RunBearerClientConfig, -} from "@corbits/workflows/client"; +} from "./run-bearer"; export interface CapabilityToolClientConfig extends RunBearerClientConfig { /** The hub's plain HTTP origin — same value memory-tools' `hubMemoryUrl` diff --git a/packages/capability-tools/src/run-bearer.ts b/packages/capability-tools/src/run-bearer.ts new file mode 100644 index 0000000000..897d14f1cf --- /dev/null +++ b/packages/capability-tools/src/run-bearer.ts @@ -0,0 +1,43 @@ +// Shared fetch plumbing for a run-authenticated tool client — every tool +// bundle that calls a hub route mounted behind a workflow-run +// authenticator (sidecar bearer token + run address, never a human +// session) needs the same two headers and the same +// `{ error: { code, userMessage } }` envelope parse. Kept local to this +// package (rather than imported from Workbench's own `@corbits/workflows`, +// which is private and carries a full server dependency graph) so this +// bundle installs standalone in any Interchange-backed hub. +import { type } from "arktype"; + +export interface RunBearerClientConfig { + readonly sidecarToken: string; + readonly address: string; + /** Override for tests; defaults to the global `fetch`. */ + readonly fetchImpl?: typeof fetch; +} + +/** The two headers every run-authenticated route resolves tenant and + * principal from — identity never rides in a request body. */ +export function runBearerHeaders( + config: RunBearerClientConfig, +): Record { + return { + authorization: `Bearer ${config.sidecarToken}`, + "x-workflow-run-address": config.address, + }; +} + +const RunBearerErrorEnvelope = type({ + error: { code: "string", userMessage: "string" }, +}); + +/** Pulls `error.userMessage` out of the canonical hub envelope + * (`{ error: { code, userMessage } }`), if `body` matches that shape — + * `undefined` for a differently-shaped or absent body, never a throw. */ +export function runBearerErrorMessage(body: unknown): string | undefined { + const parsed = RunBearerErrorEnvelope(body); + return parsed instanceof type.errors ? undefined : parsed.error.userMessage; +} + +export function runBearerFetch(config: RunBearerClientConfig): typeof fetch { + return config.fetchImpl ?? fetch; +} diff --git a/packages/capability-tools/tsconfig.json b/packages/capability-tools/tsconfig.json index d7611c1229..0815303630 100644 --- a/packages/capability-tools/tsconfig.json +++ b/packages/capability-tools/tsconfig.json @@ -1,8 +1,23 @@ { - "extends": "../../tsconfig.base.json", - "include": ["src", "test"], + "extends": "./tsconfig.src.json", "compilerOptions": { - "types": ["bun"], - "noEmit": true - } + "composite": false, + "noEmit": true, + "disableSourceOfProjectReferenceRedirect": true, + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false, + "tsBuildInfoFile": "tsconfig.tsbuildinfo", + "rootDir": "../.." + }, + "include": ["src"], + "exclude": [], + "references": [ + { + "path": "../../vendor/intx/agent/tsconfig.src.json" + }, + { + "path": "../../vendor/intx/types/tsconfig.src.json" + } + ] } diff --git a/packages/capability-tools/tsconfig.src.json b/packages/capability-tools/tsconfig.src.json new file mode 100644 index 0000000000..fcda83f444 --- /dev/null +++ b/packages/capability-tools/tsconfig.src.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "package.json", "src/**/*.json"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], + "compilerOptions": { + "types": ["bun"], + "noEmit": true, + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + { + "path": "../../vendor/intx/agent/tsconfig.src.json" + }, + { + "path": "../../vendor/intx/types/tsconfig.src.json" + } + ] +} diff --git a/packages/connections-tools/package.json b/packages/connections-tools/package.json index 2af166bcd7..6599c951d5 100644 --- a/packages/connections-tools/package.json +++ b/packages/connections-tools/package.json @@ -2,7 +2,7 @@ "name": "@corbits/connections-tools", "private": true, "description": "The list_connections and request_connection tools as an @intx/agent tool bundle: an agent sees which third-party connections this workbench already has live and hands the human a deep link to connect one that isn't, reusing the existing connections plane and never inventing new state or completing OAuth itself", - "version": "0.0.8", + "version": "0.0.9", "license": "LGPL-2.1-or-later", "type": "module", "exports": { @@ -16,7 +16,6 @@ "@intx/agent": "workspace:*", "@intx/types": "workspace:*", "@corbits/connections": "workspace:*", - "@workbench/templates": "workspace:*", "arktype": "catalog:" }, "devDependencies": { diff --git a/packages/connections-tools/src/tool.test.ts b/packages/connections-tools/src/tool.test.ts index c0f7064578..5e14ab35d1 100644 --- a/packages/connections-tools/src/tool.test.ts +++ b/packages/connections-tools/src/tool.test.ts @@ -1,5 +1,7 @@ import { expect, test } from "bun:test"; import type { ToolCall } from "@intx/types/runtime"; +import { createConnectorRegistry } from "@corbits/connections/registry"; +import type { McpPreset } from "@corbits/connections/mcp-presets"; import { connectionsTools, @@ -8,11 +10,58 @@ import { type WorkflowConnectionEnv, } from "./tool"; +// A minimal registry/preset fixture, standing in for whatever connector +// set the deploying build actually wires (e.g. Workbench's +// `@workbench/templates`) — this package carries no default of its own, +// so its tests exercise the lookup logic against a small stand-in. +const TEST_CONNECTOR_REGISTRY = createConnectorRegistry({ + github: { + id: "github", + credentialPlugin: "http", + displayName: "GitHub", + authKind: "api-key", + docsUrl: "https://github.com/settings/tokens", + feedsTools: ["@corbits/github-tools"], + probe: async () => ({ ok: true }), + }, +}); + +const TEST_MCP_PRESETS: readonly McpPreset[] = [ + { + slug: "granola", + displayName: "Granola", + description: "Search meeting notes, transcripts, and action items.", + url: "https://mcp.granola.ai/mcp", + connectionMode: "oauth", + docsUrl: "https://docs.granola.ai/help-center/sharing/integrations/mcp", + nativeConnectorId: "granola", + }, + { + slug: "exa", + displayName: "Exa", + description: "Search and research the live web.", + url: "https://mcp.exa.ai/mcp", + connectionMode: "keyless", + docsUrl: "https://docs.exa.ai/reference/exa-mcp", + nativeConnectorId: "exa", + }, + { + slug: "notion", + displayName: "Notion", + description: "Search and update pages, databases, and workspace content.", + url: "https://mcp.notion.com/mcp", + connectionMode: "oauth", + docsUrl: "https://developers.notion.com/guides/mcp/get-started-with-mcp", + }, +]; + function testEnv(): WorkflowConnectionEnv { return { hubConnectionsUrl: "https://hub.example.com", sidecarToken: "sc-token", address: "run_1@workflow", + connectorRegistry: TEST_CONNECTOR_REGISTRY, + mcpPresets: TEST_MCP_PRESETS, } as unknown as WorkflowConnectionEnv; } diff --git a/packages/connections-tools/src/tool.ts b/packages/connections-tools/src/tool.ts index 5e3be13aa8..2608617beb 100644 --- a/packages/connections-tools/src/tool.ts +++ b/packages/connections-tools/src/tool.ts @@ -31,10 +31,8 @@ import { defineTool } from "@intx/agent"; import type { BaseEnv } from "@intx/agent"; import type { ToolCall, ToolResult } from "@intx/types/runtime"; import { mcpPresetByName } from "@corbits/connections/mcp-presets"; -import { - CONNECTOR_REGISTRY, - MCP_PRESETS, -} from "@workbench/templates/connectors"; +import type { ConnectorRegistry } from "@corbits/connections/registry"; +import type { McpPreset } from "@corbits/connections/mcp-presets"; import { type } from "arktype"; import { @@ -56,6 +54,14 @@ export interface WorkflowConnectionEnv extends BaseEnv { readonly hubConnectionsUrl: string; readonly sidecarToken: string; readonly address: string; + /** The deploying build's connector set — this package carries no + * default registry of its own (that would hardcode one product's + * connector lineup into a bundle any Interchange-backed hub should + * be able to install), so the caller wiring this env supplies its + * own, e.g. Workbench's `@workbench/templates`' `CONNECTOR_REGISTRY`. */ + readonly connectorRegistry: ConnectorRegistry; + /** Same reasoning as `connectorRegistry`, for curated MCP presets. */ + readonly mcpPresets: readonly McpPreset[]; } const RequestConnectionInput = type({ @@ -107,12 +113,14 @@ function presetDeepLink(slug: string): string { return `/plugins?connect=mcp:${slug}`; } -/** Every `CONNECTOR_REGISTRY` id a curated MCP preset now fronts — - * excluded from the raw api-key connector tallies below so a service - * with both an old api-key entry and a new preset (Granola, Exa, - * Linear) is only ever reported once, under its preset's own connected/ - * not-connected state. */ -const PRESET_FRONTED_IDS = new Set(MCP_PRESETS.map((preset) => preset.slug)); +/** Every connector id a curated MCP preset now fronts — excluded from + * the raw api-key connector tallies below so a service with both an + * old api-key entry and a new preset (Granola, Exa, Linear) is only + * ever reported once, under its preset's own connected/not-connected + * state. */ +function presetFrontedIds(mcpPresets: readonly McpPreset[]): Set { + return new Set(mcpPresets.map((preset) => preset.slug)); +} async function runListConnections( env: WorkflowConnectionEnv, @@ -123,14 +131,15 @@ async function runListConnections( listConnections(clientConfig(env)), listMcpServerConnections(clientConfig(env)), ]); + const presetFronted = presetFrontedIds(env.mcpPresets); const connectedMcpSlugs = new Set(mcpServers.map((server) => server.slug)); const registryEntries = connections.filter( - (entry) => !PRESET_FRONTED_IDS.has(entry.id), + (entry) => !presetFronted.has(entry.id), ); const connected = registryEntries.filter((entry) => entry.connected); const notConnected = registryEntries.filter((entry) => !entry.connected); const otherMcpServers = mcpServers.filter( - (server) => !PRESET_FRONTED_IDS.has(server.slug), + (server) => !presetFronted.has(server.slug), ); // A preset service counts as connected through EITHER door: its MCP // server, or a plain key stored under the same connector id — a @@ -139,11 +148,11 @@ async function runListConnections( const keyConnectedIds = new Set( connections.filter((entry) => entry.connected).map((entry) => entry.id), ); - const presetConnected = MCP_PRESETS.filter( + const presetConnected = env.mcpPresets.filter( (preset) => connectedMcpSlugs.has(preset.slug) || keyConnectedIds.has(preset.slug), ); - const presetNotConnected = MCP_PRESETS.filter( + const presetNotConnected = env.mcpPresets.filter( (preset) => !connectedMcpSlugs.has(preset.slug) && !keyConnectedIds.has(preset.slug), @@ -153,7 +162,7 @@ async function runListConnections( connected.length === 0 && notConnected.length === 0 && otherMcpServers.length === 0 && - MCP_PRESETS.length === 0 + env.mcpPresets.length === 0 ) { return { callId: call.id, @@ -235,7 +244,7 @@ async function runRequestConnection( // to "connect Exa" should land on that MCP card, not the old api-key // one, which `settings-ui`/`plugins-ui` no longer render as a separate // card at all. - const preset = mcpPresetByName(MCP_PRESETS, parsed.connector); + const preset = mcpPresetByName(env.mcpPresets, parsed.connector); if (preset !== undefined) { try { const mcpServers = await listMcpServerConnections(clientConfig(env)); @@ -264,7 +273,7 @@ async function runRequestConnection( ); } - const descriptor = CONNECTOR_REGISTRY[parsed.connector]; + const descriptor = env.connectorRegistry[parsed.connector]; if (descriptor !== undefined) { try { const connections = await listConnections(clientConfig(env)); diff --git a/packages/workflow-authoring-tools/package.json b/packages/workflow-authoring-tools/package.json index 18f3e970a1..66e5d66a0f 100644 --- a/packages/workflow-authoring-tools/package.json +++ b/packages/workflow-authoring-tools/package.json @@ -2,7 +2,7 @@ "name": "@corbits/workflow-authoring-tools", "private": true, "description": "Myra's workflow-authoring tool bundle (workflow_author, workflow_republish, workflow_source_read, workflow_deploy_preview, workflow_deploy): an @intx/agent tool bundle calling @corbits/workflows's `./authoring`'s workflow-run-authenticated routes so an agent can write a workflow code package into a kind:\"workflow\" hub asset, read it back, preview a deploy's grant surface, and deploy it through Interchange's native source pipeline behind a human approval", - "version": "0.0.5", + "version": "0.0.6", "license": "LGPL-2.1-or-later", "type": "module", "exports": { @@ -13,7 +13,6 @@ "test": "bun test" }, "dependencies": { - "@corbits/workflows": "workspace:*", "@intx/agent": "workspace:*", "@intx/types": "workspace:*", "arktype": "catalog:" diff --git a/packages/workflow-authoring-tools/src/client.ts b/packages/workflow-authoring-tools/src/client.ts index 3ffc13bad0..9bfaac16fd 100644 --- a/packages/workflow-authoring-tools/src/client.ts +++ b/packages/workflow-authoring-tools/src/client.ts @@ -9,7 +9,7 @@ import { runBearerHeaders, runBearerFetch, type RunBearerClientConfig, -} from "@corbits/workflows/client"; +} from "./run-bearer"; export interface WorkflowAuthoringClientConfig extends RunBearerClientConfig { /** The hub's plain HTTP origin, the same value every other tool diff --git a/packages/workflow-authoring-tools/src/run-bearer.ts b/packages/workflow-authoring-tools/src/run-bearer.ts new file mode 100644 index 0000000000..ca98c4882b --- /dev/null +++ b/packages/workflow-authoring-tools/src/run-bearer.ts @@ -0,0 +1,28 @@ +// Shared fetch plumbing for a run-authenticated tool client — every tool +// bundle that calls a hub route mounted behind a workflow-run +// authenticator (sidecar bearer token + run address, never a human +// session) needs the same two headers. Kept local to this package +// (rather than imported from Workbench's own `@corbits/workflows`, which +// is private and carries a full server dependency graph) so this bundle +// installs standalone in any Interchange-backed hub. +export interface RunBearerClientConfig { + readonly sidecarToken: string; + readonly address: string; + /** Override for tests; defaults to the global `fetch`. */ + readonly fetchImpl?: typeof fetch; +} + +/** The two headers every run-authenticated route resolves tenant and + * principal from — identity never rides in a request body. */ +export function runBearerHeaders( + config: RunBearerClientConfig, +): Record { + return { + authorization: `Bearer ${config.sidecarToken}`, + "x-workflow-run-address": config.address, + }; +} + +export function runBearerFetch(config: RunBearerClientConfig): typeof fetch { + return config.fetchImpl ?? fetch; +} diff --git a/packages/workflow-authoring-tools/tsconfig.json b/packages/workflow-authoring-tools/tsconfig.json index d7611c1229..0815303630 100644 --- a/packages/workflow-authoring-tools/tsconfig.json +++ b/packages/workflow-authoring-tools/tsconfig.json @@ -1,8 +1,23 @@ { - "extends": "../../tsconfig.base.json", - "include": ["src", "test"], + "extends": "./tsconfig.src.json", "compilerOptions": { - "types": ["bun"], - "noEmit": true - } + "composite": false, + "noEmit": true, + "disableSourceOfProjectReferenceRedirect": true, + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false, + "tsBuildInfoFile": "tsconfig.tsbuildinfo", + "rootDir": "../.." + }, + "include": ["src"], + "exclude": [], + "references": [ + { + "path": "../../vendor/intx/agent/tsconfig.src.json" + }, + { + "path": "../../vendor/intx/types/tsconfig.src.json" + } + ] } diff --git a/packages/workflow-authoring-tools/tsconfig.src.json b/packages/workflow-authoring-tools/tsconfig.src.json new file mode 100644 index 0000000000..fcda83f444 --- /dev/null +++ b/packages/workflow-authoring-tools/tsconfig.src.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "package.json", "src/**/*.json"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], + "compilerOptions": { + "types": ["bun"], + "noEmit": true, + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + { + "path": "../../vendor/intx/agent/tsconfig.src.json" + }, + { + "path": "../../vendor/intx/types/tsconfig.src.json" + } + ] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index e52605d6cd..fa02b4ebdc 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -34,6 +34,9 @@ { "path": "./packages/bench-ui/tsconfig.src.json" }, + { + "path": "./packages/capability-tools/tsconfig.src.json" + }, { "path": "./packages/catalog-tools/tsconfig.src.json" }, @@ -175,6 +178,9 @@ { "path": "./packages/web-search-tools/tsconfig.src.json" }, + { + "path": "./packages/workflow-authoring-tools/tsconfig.src.json" + }, { "path": "./packages/xai-provider/tsconfig.src.json" }, diff --git a/workflows/assistant/package.json b/workflows/assistant/package.json index 2bcbf49d8e..e975da3746 100644 --- a/workflows/assistant/package.json +++ b/workflows/assistant/package.json @@ -27,8 +27,10 @@ }, "devDependencies": { "@corbits/agent-directory-tools": "workspace:*", + "@corbits/connections": "workspace:*", "@corbits/connections-tools": "workspace:*", "@corbits/interaction-tools": "workspace:*", + "@workbench/templates": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:" } diff --git a/workflows/assistant/src/index.ts b/workflows/assistant/src/index.ts index 0eec7dbf19..b60294d2f4 100644 --- a/workflows/assistant/src/index.ts +++ b/workflows/assistant/src/index.ts @@ -48,15 +48,15 @@ export const ASSISTANT_STEP_ID = "assistant"; */ export const ASSISTANT_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ { name: "@corbits/memory-tools", version: "0.0.4" }, - { name: "@corbits/capability-tools", version: "0.0.5" }, + { name: "@corbits/capability-tools", version: "0.0.6" }, { name: "@corbits/agent-directory-tools", version: "0.0.6" }, - { name: "@corbits/connections-tools", version: "0.0.8" }, + { name: "@corbits/connections-tools", version: "0.0.9" }, { name: "@corbits/catalog-tools", version: "0.0.6" }, { name: "@corbits/skills-tools", version: "0.0.7" }, { name: "@corbits/mcp-tools", version: "0.0.11" }, { name: "@corbits/interaction-tools", version: "0.0.8" }, { name: "@corbits/manus-tools", version: "0.0.11" }, - { name: "@corbits/workflow-authoring-tools", version: "0.0.5" }, + { name: "@corbits/workflow-authoring-tools", version: "0.0.6" }, { name: "@corbits/access-tools", version: "0.0.3" }, ]; diff --git a/workflows/assistant/test/manager-tools-scenario.test.ts b/workflows/assistant/test/manager-tools-scenario.test.ts index b0859e3314..ae2ad0b8dd 100644 --- a/workflows/assistant/test/manager-tools-scenario.test.ts +++ b/workflows/assistant/test/manager-tools-scenario.test.ts @@ -39,6 +39,14 @@ import { REQUEST_CONNECTION_TOOL, type WorkflowConnectionEnv, } from "@corbits/connections-tools"; +// `@corbits/connections-tools` carries no connector set of its own (it +// takes one through its env, so any Interchange-backed hub can install +// it with its own connector lineup) — this scenario plays back against +// Workbench's actual shipped set, so it wires in the real thing. +import { + CONNECTOR_REGISTRY, + MCP_PRESETS, +} from "@workbench/templates/connectors"; import { agentDirectoryTools, CREATE_AGENT_TOOL, @@ -237,6 +245,8 @@ async function runScenario( hubConnectionsUrl: HUB, sidecarToken: "sc-token", address: "run_myra@workflow", + connectorRegistry: CONNECTOR_REGISTRY, + mcpPresets: MCP_PRESETS, } as unknown as WorkflowConnectionEnv; const agentDirectoryEnv: WorkflowAgentDirectoryEnv = { hubAgentDirectoryUrl: HUB,