import { Effect } from "effect"; import { Data } from "effect"; import type { InvokeOptions } from "@executor-js/sdk"; import type { HandleBridge, HandleRootSpec } from "../seams/tool-sandbox"; import { ToolSandboxError } from "../seams/tool-sandbox"; import type { IntegrationDecl } from "../pipeline/descriptor"; // --------------------------------------------------------------------------- // Integration DI: declare in source, choose at invocation. // // A tool descriptor declares `integrations` (role -> integration slug). The // caller supplies one connection address per role in the tool-call payload, and // this layer peels those role properties off before the handler runs. If an org // has exactly one connection for a role's integration, that connection is used // as the default. Missing, unknown, or wrong-integration choices are typed // BindingError failures. The handler receives clients under the declared role // names. // --------------------------------------------------------------------------- export class BindingError extends Data.TaggedError("BindingError")<{ readonly message: string; readonly role: string; readonly integration: string; readonly requestedConnection?: string; }> {} export interface ConnectionCandidate { readonly address: string; readonly integration: string; readonly name?: string; readonly owner?: string; } /** The resolved connections for a tool invocation: role -> connection address. */ export type RoleBindings = Readonly>; /** Resolves connection inventory and method calls for declared integration * roles. Host wiring will back this with core connection lookup and normal * policy/credential invocation. */ export interface ClientResolver { readonly listConnections: (input: { readonly integration: string; }) => Effect.Effect; readonly resolveConnection: (input: { readonly connection: string; }) => Effect.Effect; readonly call: (input: { readonly integration: string; readonly connection: string; readonly path: readonly string[]; readonly args: readonly unknown[]; readonly invokeOptions?: InvokeOptions; }) => Effect.Effect; } export interface BindingContext { /** Declared integration roles from the tool descriptor. */ readonly declared: Readonly>; /** Resolved role -> connection address choices for this invocation. */ readonly bindings: RoleBindings; /** Routes a bound method call to the real integration. */ readonly resolver: ClientResolver; /** Caller-supplied invoke options to preserve approval/elicitation context. */ readonly invokeOptions?: InvokeOptions; } const isRecord = (value: unknown): value is Record => value !== null && typeof value === "object" && !Array.isArray(value); const findDefaultConnection = ( role: string, decl: IntegrationDecl, resolver: ClientResolver, ): Effect.Effect => resolver.listConnections({ integration: decl.integration }).pipe( Effect.flatMap((connections) => { if (connections.length === 1) return Effect.succeed(connections[0]!.address); return Effect.fail( new BindingError({ role, integration: decl.integration, message: connections.length === 0 ? `missing required connection for role "${role}" (${decl.integration}); no connections are available` : `missing required connection for role "${role}" (${decl.integration}); choose one of ${connections .map((c) => c.address) .join(", ")}`, }), ); }), ); const resolveRequestedConnection = ( role: string, decl: IntegrationDecl, requested: string, resolver: ClientResolver, ): Effect.Effect => Effect.gen(function* () { const connection = yield* resolver.resolveConnection({ connection: requested }); if (connection) { if (connection.integration !== decl.integration) { return yield* new BindingError({ role, integration: decl.integration, message: `connection "${requested}" belongs to integration "${connection.integration}", not "${decl.integration}" for role "${role}"`, requestedConnection: requested, }); } return connection.address; } const connections = yield* resolver.listConnections({ integration: decl.integration }); return yield* new BindingError({ role, integration: decl.integration, message: `unknown connection "${requested}" for role "${role}" (${decl.integration})${connections.length > 0 ? `; choose one of ${connections.map((candidate) => candidate.address).join(", ")}` : ""}`, requestedConnection: requested, }); }); export interface ResolvedIntegrationBindings { readonly input: unknown; readonly bindings: RoleBindings; } /** Peel platform-added role properties from the caller payload and resolve them * to concrete connection addresses. */ export const resolveIntegrationBindings = ( declared: Readonly>, args: unknown, resolver: ClientResolver, ): Effect.Effect => Effect.gen(function* () { const roles = Object.entries(declared); if (roles.length === 0) return { input: args, bindings: {} }; const payload = isRecord(args) ? args : {}; const input: Record = {}; for (const [key, value] of Object.entries(payload)) { if (!Object.prototype.hasOwnProperty.call(declared, key)) input[key] = value; } const bindings: Record = {}; for (const [role, decl] of roles) { const raw = payload[role]; if (raw === undefined) { bindings[role] = yield* findDefaultConnection(role, decl, resolver); continue; } if (typeof raw !== "string" || raw.length === 0) { return yield* new BindingError({ role, integration: decl.integration, message: `connection for role "${role}" (${decl.integration}) must be a non-empty string`, ...(typeof raw === "string" ? { requestedConnection: raw } : {}), }); } bindings[role] = yield* resolveRequestedConnection(role, decl, raw, resolver); } return { input, bindings }; }); /** Compute the sandbox handle roots: one single root per declared integration * role. */ export const rootsFor = ( declared: Readonly>, ): Readonly> => { const roots: Record = {}; for (const role of Object.keys(declared)) { roots[role] = { kind: "single" }; } return roots; }; // Parse an indexed root name. Indexed roots are not supported in v1, but the // parser lets the bridge reject `role#0` explicitly instead of misrouting it. const parseRoot = (root: string): { role: string; index?: number } => { const hash = root.indexOf("#"); if (hash === -1) return { role: root }; return { role: root.slice(0, hash), index: Number(root.slice(hash + 1)) }; }; const RESERVED_ROOTS = new Set(["__proto__", "constructor", "prototype"]); const invokeErr = (message: string): ToolSandboxError => new ToolSandboxError({ kind: "invoke", message }); /** * Build the HandleBridge the sandbox calls out through. A declared role routes * to its resolved connection through the `ClientResolver`. The dispatch is * STRICT: an empty/malformed path, a reserved root, an undeclared root, or an * indexed root is an error, never a silent success. A `.account` read on a * client returns bound-connection metadata without a round-trip. */ export const buildBridge = (context: BindingContext): HandleBridge => ({ call: ({ root, path, args }) => { if (typeof root !== "string" || root.length === 0) { return Effect.fail(invokeErr("bridge call is missing a handle root")); } if (!Array.isArray(path) || path.length === 0) { return Effect.fail(invokeErr(`bridge call to "${root}" has an empty method path`)); } if (RESERVED_ROOTS.has(root)) { return Effect.fail(invokeErr(`reserved handle root is not callable: ${root}`)); } const { role, index } = parseRoot(root); if (index !== undefined) { return Effect.fail( invokeErr(`single integration role "${role}" was addressed with an index`), ); } const decl = context.declared[role]; if (!decl) { return Effect.fail(invokeErr(`undeclared handle root: ${root}`)); } const connectionName = context.bindings[role]; if (!connectionName) { return Effect.fail(invokeErr(`no binding for role ${role}`)); } if (path.length === 1 && path[0] === "account") { // Connection-derived placeholders only; this is not verified upstream // account identity. return Effect.succeed({ email: connectionName, login: connectionName, name: connectionName }); } return context.resolver .call({ integration: decl.integration, connection: connectionName, path, args, invokeOptions: context.invokeOptions, }) .pipe( Effect.mapError( (cause) => new ToolSandboxError({ kind: "invoke", message: "integration call failed", cause, }), ), ); }, });