888 lines · 30.9 KB · TypeScript
RawDownload
import { Effect, Predicate } from "effect";import * as Cause from "effect/Cause";import type {  Executor,  InvokeOptions,  Integration,  ToolError,  Tool,  ToolSchemaView,} from "@executor-js/sdk/core";import {  annotateToolResultOutcome,  authToolFailure,  isUserActionableError,  isToolResult,  ToolResult,  ToolAddress,  parseToolAddress,} from "@executor-js/sdk/core";import type { SandboxToolInvoker } from "@executor-js/codemode-core";import { ExecutionToolError } from "./errors"; const OPAQUE_DEFECT_MESSAGE = "Internal tool error";const TOOL_DESCRIBE_SUGGESTION_LIMIT = 5;const TOOL_ERROR_TYPESCRIPT =  "{ code: string; message: string; status?: number; details?: unknown; retryable?: boolean }";// Present on HTTP-backed tools (OpenAPI): transport facts beside the payload// so callers can read pagination/rate-limit headers without the payload// being wrapped in an envelope.const TOOL_HTTP_META_TYPESCRIPT = "{ status: number; headers: { [k: string]: string; } }";const TOOL_FILE_TYPESCRIPT =  '{ _tag: "ToolFile"; name?: string; mimeType: string; encoding: "base64"; data: string; byteLength: number; }'; const wrapOutputTypeScript = (outputTypeScript?: string, marker?: string): string =>  `{ ok: true; data: ${outputTypeScript ?? "unknown"}${marker ?? ""}; http?: ToolHttpMeta } | { ok: false; error: ToolError }`; /** Inline provenance for observed types — a model that copies only the type *  string still sees the hint, since the compact render drops descriptions. */const OBSERVED_TYPE_MARKER = " /* observed; may be incomplete */"; const withToolResultDefinitions = (  definitions?: Record<string, string>,): Record<string, string> => ({  ...(definitions ?? {}),  ToolError: TOOL_ERROR_TYPESCRIPT,  ToolHttpMeta: TOOL_HTTP_META_TYPESCRIPT,  ToolFile: TOOL_FILE_TYPESCRIPT,}); const ADDRESS_PREFIX = "tools."; /** * Map a sandbox tool path to the executor's `execute` address. * * v2 dynamic tools are addressed `tools.<integration>.<owner>.<connection>.<tool>`. * The sandbox proxy strips the leading `tools.` (the proxy root), so a model * writing `tools.github.org.main.getRepo(args)` produces the path * `github.org.main.getRepo`. Re-prefix it so it parses as a 5-segment address. * * Plugin-contributed static tools (core-tools under `executor`, plugin executor * namespaces) are addressed by their fqid with no prefix; the executor resolves * those from its static map directly, so leave them untouched. */const pathToAddress = (path: string): ToolAddress => {  if (path.startsWith(ADDRESS_PREFIX)) return ToolAddress.make(path);  if (parseToolAddress(`${ADDRESS_PREFIX}${path}`)) {    return ToolAddress.make(`${ADDRESS_PREFIX}${path}`);  }  return ToolAddress.make(path);}; /** Strip the proxy-root `tools.` prefix from a full address so it becomes the *  sandbox-callable path the model writes after `tools.`. */const addressToPath = (address: string): string =>  address.startsWith(ADDRESS_PREFIX) ? address.slice(ADDRESS_PREFIX.length) : address; type DescribedTool = {  readonly path: string;  readonly name: string;  readonly description?: string;  readonly inputTypeScript?: string;  readonly outputTypeScript?: string;  readonly outputTypeScriptNote?: string;  readonly typeScriptDefinitions?: Record<string, string>;  /** Set when the path resolves to no tool — mirrors invoke's tool_not_found. */  readonly error?: {    readonly code: "tool_not_found";    readonly message: string;    readonly suggestions?: readonly string[];  };}; const BUILTIN_TOOL_DESCRIPTIONS: ReadonlyMap<string, DescribedTool> = new Map<  string,  DescribedTool>([  [    "search",    {      path: "search",      name: "search",      description:        "Search available Executor tools. An empty query with a namespace enumerates that integration's full catalog, sorted by path.",      inputTypeScript: "{ query: string; namespace?: string; limit?: number; offset?: number; }",      outputTypeScript:        "{ items: ToolDiscoveryResult[]; total: number; hasMore: boolean; nextOffset: number | null; }",      typeScriptDefinitions: {        ToolDiscoveryResult:          "{ path: string; name: string; description?: string; integration: string; score: number; }",      },    },  ],  [    "executor.integrations.list",    {      path: "executor.integrations.list",      name: "executor.integrations.list",      description: "List configured Executor integrations.",      inputTypeScript: "{ query?: string; limit?: number; offset?: number; }",      outputTypeScript:        "{ items: ExecutorIntegrationListItem[]; total: number; hasMore: boolean; nextOffset: number | null; }",      typeScriptDefinitions: {        ExecutorIntegrationListItem:          "{ id: string; name: string; description?: string; kind: string; canRemove?: boolean; canRefresh?: boolean; toolCount: number; }",      },    },  ],  [    "describe.tool",    {      path: "describe.tool",      name: "describe.tool",      description: "Describe a tool's compact TypeScript input and output shapes.",      inputTypeScript: "{ path: string; }",      outputTypeScript: "DescribedTool",      typeScriptDefinitions: {        DescribedTool:          '{ path: string; name: string; description?: string; inputTypeScript?: string; outputTypeScript?: string; typeScriptDefinitions?: { [k: string]: string; }; error?: { code: "tool_not_found"; message: string; suggestions?: string[]; }; }',      },    },  ],]); const newCorrelationId = (): string => {  // 8-hex-char correlation id; enough entropy to disambiguate within a  // single deployment without leaking host process info.  return Math.floor(Math.random() * 0x1_0000_0000)    .toString(16)    .padStart(8, "0");}; const validationIssues = (value: unknown): readonly unknown[] | null => {  if (typeof value !== "object" || value === null) return null;  const issues = (value as { readonly issues?: unknown }).issues;  return Array.isArray(issues) ? issues : null;}; // Pre-flight OpenAPI invocation failures (missing/unresolved path params,// missing or malformed request body) are caller-argument errors raised before// any HTTP request is sent. Their messages are generated locally from the// spec (no upstream data, no transport internals), so they are safe to// surface, and actionable: the fix is renaming or adding an argument.// Discriminator: pre-flight raises carry `statusCode: None` and no `cause`.// Transport failures share `statusCode: None` but wrap the raw client error// (which can hold internal URLs) as `cause`, and post-response failures carry// a status code; both stay on the opaque-defect path. Matched structurally// because this package does not depend on the openapi plugin.const openApiPreflightMessage = (value: unknown): string | null => {  if (!Predicate.isTagged(value, "OpenApiInvocationError")) return null;  const err = value as {    readonly message?: unknown;    readonly statusCode?: unknown;    readonly cause?: unknown;  };  if (err.cause !== undefined) return null;  if (!Predicate.isTagged(err.statusCode, "None")) return null;  // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: structural match on the openapi plugin's tagged error; the pre-flight message is locally generated (no upstream data) and is the payload being surfaced  return typeof err.message === "string" && err.message.length > 0 ? err.message : null;}; const credentialResolutionToolFailure = (input: {  readonly label: string;  readonly message: string;  readonly reauthRequired?: boolean;  readonly oauthErrorCode?: string;}) =>  authToolFailure({    code: input.reauthRequired === true ? "oauth_reauth_required" : "oauth_refresh_failed",    message:      input.reauthRequired === true        ? `OAuth connection "${input.label}" requires reauthorization: ${input.message}`        : `OAuth connection "${input.label}" could not be resolved: ${input.message}`,    credential: {      kind: "oauth",      label: input.label,    },    // The AS's own RFC 6749 §5.2 verdict, when there was one — structured so    // the agent (and anyone reading the failure) can distinguish a dead grant    // from a misconfigured app without parsing the message.    ...(input.oauthErrorCode !== undefined      ? { upstream: { details: { oauthErrorCode: input.oauthErrorCode } } }      : {}),  }); const bindingToolFailure = (value: unknown): ToolError | null => {  if (!Predicate.isTagged(value, "BindingError")) return null;  const maybeBinding = value as {    readonly message?: unknown;    readonly role?: unknown;    readonly integration?: unknown;    readonly requestedConnection?: unknown;  };  const details: Record<string, string> = {};  if (typeof maybeBinding.role === "string") details.role = maybeBinding.role;  if (typeof maybeBinding.integration === "string") details.integration = maybeBinding.integration;  if (typeof maybeBinding.requestedConnection === "string") {    details.requestedConnection = maybeBinding.requestedConnection;  }  return {    code: "binding_error",    message:      typeof maybeBinding.message === "string" ? maybeBinding.message : "Tool binding failed.",    ...(Object.keys(details).length > 0 ? { details } : {}),  };}; const expectedToolFailure = (value: unknown): ToolError | null => {  if (isUserActionableError(value)) {    return {      code: value.code,      message: value.userMessage,    };  }  if (Predicate.isTagged(value, "ToolNotFoundError") && "address" in value) {    const suggestions =      "suggestions" in value && Array.isArray(value.suggestions)        ? value.suggestions.map((suggestion) => addressToPath(String(suggestion)))        : undefined;    const address = addressToPath(String(value.address));    return {      code: "tool_not_found",      message: `Tool not found: ${address}`,      details: { path: address, ...(suggestions ? { suggestions } : {}) },    };  }  if (Predicate.isTagged(value, "ToolBlockedError") && "address" in value) {    return {      code: "tool_blocked",      message: `Tool blocked by policy: ${addressToPath(String(value.address))}`,      details: value,    };  }  if (Predicate.isTagged(value, "ToolInvocationError")) {    const cause = (value as { readonly cause?: unknown }).cause;    if (isUserActionableError(cause)) {      return {        code: cause.code,        message: cause.userMessage,      };    }    const binding = bindingToolFailure(cause);    if (binding) return binding;    const issues = validationIssues(cause);    if (issues) {      return {        code: "invalid_tool_arguments",        message: "Tool arguments did not match the input schema.",        details: { issues },      };    }    const preflight = openApiPreflightMessage(cause);    if (preflight) {      return {        code: "invalid_tool_arguments",        message: preflight,      };    }  }  return null;}; /** * Extract the integration namespace from a tool path. v2 addresses look like * `<integration>.<owner>.<connection>.<tool>`; static fqids look like * `<integration>.<op>`. We take the first segment as a cheap, non-lookup namespace * for the span attribute so it's always populated without a catalog read. */const extractNamespace = (path: string): string => {  const normalized = addressToPath(path);  const idx = normalized.indexOf(".");  return idx === -1 ? normalized : normalized.slice(0, idx);}; /** * Bridges QuickJS `tools.<integration>.<owner>.<connection>.<tool>(args)` calls * into `executor.execute(address, args)`. * * Wrapped in `Effect.fn("mcp.tool.dispatch")` so every tool call becomes a * span in the Effect tracer. Attributes: *   - `mcp.tool.name`         — full tool path (e.g. "github.org.main.getRepo") *   - `mcp.tool.integration`  — first segment of the path (namespace) * * `mcp.tool.kind` (openapi | mcp | graphql | code) is NOT annotated here * because it would require an `integrations.list()` lookup on every invocation. * Callers that already know the integration kind can annotate at their own span. */export const makeExecutorToolInvoker = (  executor: Executor,  options: { readonly invokeOptions: InvokeOptions },): SandboxToolInvoker => ({  invoke: Effect.fn("mcp.tool.dispatch")(function* ({ path, args }) {    yield* Effect.annotateCurrentSpan({      "mcp.tool.name": path,      "mcp.tool.integration": extractNamespace(path),    });     const address = pathToAddress(path);    const result = yield* executor.execute(address, args, options.invokeOptions).pipe(      Effect.catchTag("CredentialResolutionError", (err) =>        Effect.succeed(          credentialResolutionToolFailure({            label: `${err.integration}.${err.owner}.${err.name}`,            message: err.message,            reauthRequired: err.reauthRequired,            oauthErrorCode: err.oauthErrorCode,          }),        ),      ),      Effect.catchCause((cause) => {        const err = cause.reasons.find(Cause.isFailReason)?.error;        const expected = expectedToolFailure(err);        if (expected) {          return Effect.succeed(ToolResult.fail(expected));        }        if (isElicitationDeclinedError(err)) {          return Effect.fail(            new ExecutionToolError({              message: `Tool "${addressToPath(String(err.address))}" requires approval but the request was ${err.action === "cancel" ? "cancelled" : "declined"} by the user.`,              cause: err,            }),          );        }        // Any other failure here is an infra/plugin defect. Emit an        // opaque generic with a correlation id so internal context (URLs        // with tokens, DB connection strings, file paths in stacks)        // can't leak through Error.message into the sandbox. The full        // cause is logged with the same correlation id so operators can        // still trace the failure.        const correlationId = newCorrelationId();        return Effect.logError("tool dispatch failed", cause).pipe(          Effect.annotateLogs({            "executor.correlation_id": correlationId,            "mcp.tool.name": path,          }),          Effect.flatMap(() =>            Effect.fail(              new ExecutionToolError({                message: `${OPAQUE_DEFECT_MESSAGE} [${correlationId}]`,                cause: err ?? cause,              }),            ),          ),        );      }),    );     // Strict: plugins emit ToolResult<T>. Anything else is treated as a    // raw success value and wrapped — keeps the sandbox-facing contract    // uniform without forcing every tiny test plugin to import    // `ToolResult.ok`.    // Expected failures resolve through the success channel, so without the    // outcome annotation the dispatch span reads as healthy even when the    // caller hit an upstream error or auth wall.    yield* annotateToolResultOutcome(result);    if (isToolResult(result)) {      return result;    }    return { ok: true, data: result };  }),}); const isElicitationDeclinedError = (  value: unknown,): value is {  readonly _tag: "ElicitationDeclinedError";  readonly address: string;  readonly action: "cancel" | "decline";} =>  Predicate.isTagged(value, "ElicitationDeclinedError") &&  value !== null &&  typeof value === "object" &&  "address" in value &&  typeof value.address === "string" &&  "action" in value &&  (value.action === "cancel" || value.action === "decline"); export type ToolDiscoveryResult = {  readonly path: string;  readonly name: string;  readonly description?: string;  readonly integration: string;  readonly score: number;}; export type ExecutorIntegrationListItem = {  readonly id: string;  readonly name: string;  readonly description?: string;  readonly kind: string;  readonly canRemove?: boolean;  readonly canRefresh?: boolean;  readonly toolCount: number;}; export type ToolDiscoveryInput = {  readonly executor: Executor;  readonly query: string;  readonly namespace?: string;  readonly limit: number;  readonly offset: number;}; export interface ToolDiscoveryProvider {  readonly searchTools: (    input: ToolDiscoveryInput,  ) => Effect.Effect<PagedResult<ToolDiscoveryResult>, ExecutionToolError>;} /** * Page of results from a list-style discovery tool. Shared by * `tools.search` and `tools.executor.integrations.list` so the model sees one * consistent shape: * *   - `items`      — the page (slice). *   - `total`      — count after filtering, before pagination. The model *                    can use this to detect truncation. *   - `hasMore`    — convenience flag for `(offset + items.length) < total`. *   - `nextOffset` — concrete offset for the next page when `hasMore`, *                    `null` otherwise. Pre-computing it removes a class of *                    off-by-one mistakes when the model paginates. */export type PagedResult<T> = {  readonly items: readonly T[];  readonly total: number;  readonly hasMore: boolean;  readonly nextOffset: number | null;}; const paginate = <T>(all: readonly T[], offset: number, limit: number): PagedResult<T> => {  const total = all.length;  const start = Math.min(Math.max(offset, 0), total);  const items = all.slice(start, start + limit);  const consumed = start + items.length;  const hasMore = consumed < total;  return {    items,    total,    hasMore,    nextOffset: hasMore ? consumed : null,  };}; /** What `searchTools` ranks over — the sandbox-callable path plus the v2 *  identity fields a query can match against. */type SearchableTool = {  readonly path: string;  readonly integration: string;  readonly name: string;  readonly description?: string;}; const toSearchableTool = (tool: Tool): SearchableTool => ({  path: addressToPath(String(tool.address)),  integration: String(tool.integration),  name: String(tool.name),  description: tool.description,}); type PreparedField = {  readonly raw: string;  readonly tokens: readonly string[];}; const SEARCH_FIELD_WEIGHTS = {  path: 12,  integration: 8,  name: 10,  description: 5,} as const; const normalizeSearchText = (value: string): string =>  value    .replace(/([a-z0-9])([A-Z])/g, "$1 $2")    .replace(/[_./:-]+/g, " ")    .toLowerCase()    .trim(); const tokenizeSearchText = (value: string): string[] =>  normalizeSearchText(value)    .split(/[^a-z0-9]+/)

Lines 1–500 of 888.