Documentation/Scoped Connector Registry
v2.0.0-beta.1Snapshot bebdcd1Markdown ↗

Scoped Connector Registry

Scoped Connector Registry

In multi-user or multi-tenant systems, you often need to limit which connectors are visible to which users. The Scoped Connector Registry provides a pluggable access control layer over the Connector registry — a lightweight filtered view gated by a user-provided policy predicate. Zero changes to the existing API; scoping is entirely opt-in.

Access Control Policies

Define a policy that determines which connectors a given context can access:

typescript
import { Connector, ScopedConnectorRegistry } from 'oneringai';
import type { IConnectorAccessPolicy, ConnectorAccessContext } from 'oneringai';

// Tag-based policy: connector must have a matching tenant tag
const tenantPolicy: IConnectorAccessPolicy = {
  canAccess: (connector, context) => {
    const tags = connector.config.tags as string[] | undefined;
    const tenantId = context.tenantId as string;
    return !!tags && tags.includes(tenantId);
  },
};

// Role-based policy
const rolePolicy: IConnectorAccessPolicy = {
  canAccess: (connector, context) => {
    const roles = context.roles as string[];
    if (roles.includes('admin')) return true;
    // Non-admins can only see connectors in their department
    const dept = connector.config.tags as string[] | undefined;
    return !!dept && dept.includes(context.department as string);
  },
};

Policy rules:

  • canAccess() is synchronous — access checks must be fast, policy data should be in-memory
  • context is an opaque Record<string, unknown> — the library imposes no structure
  • The policy receives the full Connector instance so it can inspect config.tags, vendor, serviceType, baseURL, etc.

Setting a Global Policy

typescript
// Set the policy (required before calling Connector.scoped())
Connector.setAccessPolicy(tenantPolicy);

// Check current policy
const current = Connector.getAccessPolicy(); // IConnectorAccessPolicy | null

// Clear the policy
Connector.setAccessPolicy(null);

Creating Scoped Views

typescript
// Create a scoped view for tenant "acme"
const acmeRegistry = Connector.scoped({ tenantId: 'acme' });

// Only connectors tagged with "acme" are visible
acmeRegistry.list();       // ['acme-openai', 'acme-slack']
acmeRegistry.size();       // 2
acmeRegistry.has('other'); // false

// Accessing a denied connector gives the same "not found" error
// as a truly non-existent one — no information leakage
acmeRegistry.get('competitor-key');
// throws: "Connector 'competitor-key' not found. Available: acme-openai, acme-slack"

You can also create a ScopedConnectorRegistry directly with any policy (not just the global one):

typescript
import { ScopedConnectorRegistry } from 'oneringai';

const custom = new ScopedConnectorRegistry(myPolicy, { userId: 'user-123' });

Unfiltered Admin View

When your code accepts IConnectorRegistry but you want the full, unfiltered view:

typescript
import type { IConnectorRegistry } from 'oneringai';

// Returns an IConnectorRegistry that delegates to Connector static methods
const adminRegistry: IConnectorRegistry = Connector.asRegistry();

// Full access, no filtering
adminRegistry.list(); // all connectors

Using with Agent

Pass a scoped registry to Agent.create() via the registry option:

typescript
const registry = Connector.scoped({ tenantId: 'acme' });

const agent = Agent.create({
  connector: 'acme-openai',  // Resolved via scoped registry
  model: 'gpt-4.1',
  registry,
});

// The agent can only see connectors accessible to 'acme'

If the connector name isn't accessible through the scoped registry, agent creation throws the standard "Connector not found" error listing only visible connectors.

Using with ConnectorTools

All major ConnectorTools methods accept an optional { registry } option:

typescript
const registry = Connector.scoped({ tenantId: 'acme' });

// Get tools for a specific connector (resolved via scoped registry)
const tools = ConnectorTools.for('acme-slack', undefined, { registry });

// Discover tools for all accessible connectors
const allTools = ConnectorTools.discoverAll(undefined, { registry });

// Find connectors by service type (searches only accessible connectors)
const github = ConnectorTools.findConnector('github', { registry });
const allGithubs = ConnectorTools.findConnectors('github', { registry });

Multi-Tenant Example

typescript
import { Connector, Agent, Vendor, ConnectorTools } from 'oneringai';
import type { IConnectorAccessPolicy } from 'oneringai';

// 1. Create connectors with tenant tags
Connector.create({
  name: 'acme-openai',
  vendor: Vendor.OpenAI,
  auth: { type: 'api_key', apiKey: process.env.ACME_OPENAI_KEY! },
  tags: ['acme'],
});

Connector.create({
  name: 'globex-openai',
  vendor: Vendor.OpenAI,
  auth: { type: 'api_key', apiKey: process.env.GLOBEX_OPENAI_KEY! },
  tags: ['globex'],
});

Connector.create({
  name: 'acme-github',
  auth: { type: 'api_key', apiKey: process.env.ACME_GITHUB_TOKEN! },
  baseURL: 'https://api.github.com',
  tags: ['acme'],
});

// 2. Set up the policy
const policy: IConnectorAccessPolicy = {
  canAccess: (connector, ctx) => {
    const tags = connector.config.tags as string[] | undefined;
    return !!tags && tags.includes(ctx.tenantId as string);
  },
};
Connector.setAccessPolicy(policy);

// 3. Per-request: create a scoped view
function handleRequest(tenantId: string) {
  const registry = Connector.scoped({ tenantId });

  // This tenant can only see their own connectors
  const agent = Agent.create({
    connector: `${tenantId}-openai`,
    model: 'gpt-4.1',
    registry,
  });

  // Discover tools only for this tenant's connectors
  const tools = ConnectorTools.discoverAll(undefined, { registry });

  return { agent, tools };
}

// Acme sees: acme-openai, acme-github
handleRequest('acme');

// Globex sees: globex-openai
handleRequest('globex');

IConnectorRegistry Interface

The IConnectorRegistry interface covers the read-only subset of Connector static methods:

Method Description
get(name) Get connector by name (throws if not found/denied)
has(name) Check if connector exists and is accessible
list() List accessible connector names
listAll() List accessible connector instances
size() Count of accessible connectors
getDescriptionsForTools() Formatted descriptions for LLM tool parameters
getInfo() Connector info map for UI/documentation
getById(id) Optional ID lookup; same error contract as get(name)

Missing connectors throw the public ConnectorNotFoundError, which extends AIError and carries code: 'CONNECTOR_NOT_FOUND'. Scoped registries and agent identity views use the same error for hidden connectors; absence does not reveal whether a connector exists in another scope. The error has no HTTP status: the host decides how to report it and whether an optional read can use a separately authorized cache. It never grants access to a connector or cached data.

Custom IConnectorRegistry implementations should throw this error for missing or scope-hidden connectors. Connector.get() and Connector.getById() delegate without wrapping errors: authentication, broken configuration and storage errors must propagate as their original failures, not be converted into absence.

typescript
import { ConnectorNotFoundError } from 'oneringai';

// Inside a custom registry, after applying its scope/visibility checks:
throw new ConnectorNotFoundError();

The default message is safe and generic. A custom diagnostic message and optional original error may be supplied as new ConnectorNotFoundError(message, cause). Built-in diagnostics retain the requested name/ID and visible connector names; HTTP adapters should avoid exposing these diagnostics to clients.


Documentation snapshot · 2026-11-02Suggest a correction ↗

Search documentation

Type to search the v2 documentation.