/** * Tenant connectors — one org's own MCP server or OpenAPI spec, registered at * runtime by the host's own dev-side code. No redeploy, no console, no UI. * * Isolation is STRUCTURAL, not a filter. Each org that has registered anything * gets its OWN actions registry, built over the shared connectors PLUS its own; * a request is served the registry its ASSERTED memberships select (build * contract §9.1 — the same `memberships` the org-policy seam reads). Another * tenant's connector is not withheld from that registry, it was never in it, so * there is no filter to get wrong and no listing that could leak a name. * * Nothing here is a store schema change. The registrations live in the generic * `vendo_records` collection — `vendo_tenant_connectors` is neither reserved nor * dedicated (store/routing.ts), so it routes to `vendo_records` on every adapter * with no migration — ref'd `{ subject: org }`, which is the key the erase * cascade already matches (store/erase.ts's subject leg; an org id IS a row * subject, build contract §9.5/§9.7), so erasing an org takes its registrations * with it. * * The TOKEN never lands in a row. It is vaulted in the store's encrypted secrets * under a tenant-scoped name and read back only to build a connector — `list` * and `register` answer descriptors and metadata, never the credential. */ import { type Connector } from "./actions/index.js"; import { type RunContext, type StoreAdapter, type StoreOps, type ToolDescriptor, type ToolListingContext, type ToolRegistry, type VendoErrorCode } from "./core/index.js"; /** What the host registers: an MCP server URL or an OpenAPI spec, plus the * bearer token the tenant pasted. */ export interface TenantConnectorInput { org: string; name: string; kind: "mcp" | "openapi"; url?: string; spec?: string | Record; token?: string; } /** Register and test both answer the same way: the tools the server really * advertised, or a typed refusal. */ export type TenantConnectorResult = { status: "ok"; tools: ToolDescriptor[]; } | { status: "error"; error: { code: VendoErrorCode; message: string; }; }; /** One registration, as `list` reports it. Deliberately carries no `spec` and * no token: this is the surface an admin screen renders. */ export interface TenantConnectorSummary { org: string; name: string; kind: "mcp" | "openapi"; url?: string; registeredAt: string; } /** The dev-side API on the Vendo handle. `register` IS save-and-test: it * validates by actually connecting, so a registration that landed is a * registration that worked. */ export interface TenantConnectors { register(input: TenantConnectorInput): Promise; list(org: string): Promise; remove(org: string, name: string): Promise; test(org: string, name: string): Promise; } export interface ComposedTenantConnectors { /** The public handle. Carries no overlay affordance of any kind. */ api: TenantConnectors; /** The registries this run's asserted orgs add to the shared one, in the * order they were asserted. Empty for a run that asserts no org, or whose * orgs have registered nothing. */ overlay(ctx: ToolListingContext | RunContext | undefined): Promise; } export declare function createTenantConnectors(deps: { store: StoreAdapter; /** The store's named-operation surface — `secrets` is where the token lives. * Absent for a store that offers neither a handle nor ops, which is a store * that cannot vault a credential; `register` says so instead of dropping it. */ ops: StoreOps | undefined; /** One tenant's connectors as a registry of their own, under the same guard * binding, connect gate and generation choke the shared registry rides. */ bind: (connectors: Connector[]) => ToolRegistry; }): ComposedTenantConnectors; /** THE selection point: the shared surface, plus the registries this run's orgs * add to it. * * A merge of registries (the same shape the standalone agent surface's own * multi-source registry takes), never a filter over one combined set — a run whose orgs * registered nothing is handed the base registry untouched, and a run whose org * did is handed a registry another tenant's connector was never in. * * ONE order decides everything, and the base leads it. A name the base carries * is the base's, so a tenant server can never shadow a host tool by naming one * of its own after it; after that, the first org the caller asserted wins. The * listing and the dispatch walk that same order, so the tool a person is * offered is always the tool that runs. */ export declare function withTenantOverlay(base: ToolRegistry, overlay: ComposedTenantConnectors["overlay"]): ToolRegistry;