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
3 changes: 1 addition & 2 deletions packages/capability-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -13,7 +13,6 @@
"test": "bun test"
},
"dependencies": {
"@corbits/workflows": "workspace:*",
"@intx/agent": "workspace:*",
"@intx/types": "workspace:*",
"arktype": "catalog:"
Expand Down
2 changes: 1 addition & 1 deletion packages/capability-tools/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
43 changes: 43 additions & 0 deletions packages/capability-tools/src/run-bearer.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> {
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;
}
25 changes: 20 additions & 5 deletions packages/capability-tools/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
21 changes: 21 additions & 0 deletions packages/capability-tools/tsconfig.src.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
3 changes: 1 addition & 2 deletions packages/connections-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -16,7 +16,6 @@
"@intx/agent": "workspace:*",
"@intx/types": "workspace:*",
"@corbits/connections": "workspace:*",
"@workbench/templates": "workspace:*",
"arktype": "catalog:"
},
"devDependencies": {
Expand Down
49 changes: 49 additions & 0 deletions packages/connections-tools/src/tool.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
}

Expand Down
43 changes: 26 additions & 17 deletions packages/connections-tools/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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({
Expand Down Expand Up @@ -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<string> {
return new Set(mcpPresets.map((preset) => preset.slug));
}

async function runListConnections(
env: WorkflowConnectionEnv,
Expand All @@ -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
Expand All @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down
3 changes: 1 addition & 2 deletions packages/workflow-authoring-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -13,7 +13,6 @@
"test": "bun test"
},
"dependencies": {
"@corbits/workflows": "workspace:*",
"@intx/agent": "workspace:*",
"@intx/types": "workspace:*",
"arktype": "catalog:"
Expand Down
2 changes: 1 addition & 1 deletion packages/workflow-authoring-tools/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions packages/workflow-authoring-tools/src/run-bearer.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> {
return {
authorization: `Bearer ${config.sidecarToken}`,
"x-workflow-run-address": config.address,
};
}

export function runBearerFetch(config: RunBearerClientConfig): typeof fetch {
return config.fetchImpl ?? fetch;
}
25 changes: 20 additions & 5 deletions packages/workflow-authoring-tools/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Loading
Loading