Seams¶
The interfaces where one layer hands off to another, and where a host can plug in its own
implementation. Read the overview and the domain first. Read
the run() pipeline next.
flowchart LR
B["Binding<br/>REST · gRPC · MCP"] --> I["invoke.py"]
I --> F["ConnectorFactory"]
F --> C["Connector instance<br/>tenant-pinned"]
F -. "injects" .-> S["SecretProvider"]
F -. "injects" .-> A["AuthProvider(s)"]
F -. "injects" .-> P["PolicyHook"]
C --> E["ErrorMapper"]
classDef binding fill:#ddf1fb,stroke:#1a88b0,stroke-width:1px,color:#0d2f3d
classDef runtime fill:#fdf2d6,stroke:#b8860b,stroke-width:1px,color:#3a2c05
class B,I,F binding
class C,S,A,P,E runtime
ConnectorFactory¶
src/bindings/factory.py. It builds tenant-pinned connector instances.
load()readsconfig/connectors.yaml(orNW_CONFIG_PATH) and bootstraps it into the config store as the__default__tenant. It does not instantiate connectors.await get(connector_id, tenant_id=…, config_name=…)resolves the tenant's config and injects the tenant-scopedSecretProvider, theAuthProvider(s) and thePolicyHook. The instance is cached per(tenant, connector, config), and a store write drops the cached entry.ConnectorFactory(auth_provider_hook=…)lets a layer supply its own auth provider before the built-in ones.node-wire-toolhiveuses it for the credential relay.- Connectors come from the registry filled by
auto_register(). The factory has no per-connector branches.
Method-by-method reference: Connector reference — ConnectorFactory API.
invoke.py¶
src/bindings/invoke.py, the one path every binding uses to run an action. The binding resolves
the tenant, the named config and the caller identity first. invoke() then:
- checks
exposed_viafor the protocol (ConnectorNotExposedotherwise); - calls
factory.get(...); - runs the action's normalizers and enforces that the action named by the route or tool wins over
any
actionin the payload; - calls
run(payload, principal=…, tenant_id=…, scopes=…).
Only transport concerns stay in each binding: HTTP status mapping, MCP pagination and protobuf encoding. See Connectors on REST, MCP and gRPC.
PolicyHook¶
node_wire_runtime.policy.PolicyHook, an allow/deny check inside run() before the action executes.
It raises PolicyDenied, which maps to POLICY_DENIED. The factory always attaches one:
| Condition | Hook |
|---|---|
NW_MCP_ACTION_SCOPE_MAP_JSON set, or NW_MCP_SCOPE_POLICY_DEFAULT=deny (the default) |
ScopePolicyHook: the caller needs mcp:<connector>.<action> or the mapped scope |
Otherwise (allow with an empty map) |
TenantConfigHook: the tenant must have a config for the connector |
NW_MCP_SCOPE_POLICY_STRICT=true refuses to start in the second case. The hook applies to every
run(), in-process calls included. Variables: Configuration.
SecretProvider¶
node_wire_runtime.secrets.base.SecretProvider. Connectors read credentials only through
self.secret_provider.get_secret(key). A missing key raises SecretNotFoundError (fail-closed).
- The factory picks the backend from
NW_SECRET_BACKEND(env, oraws_envfor AWS Secrets Manager with env fallback). - For a named tenant it wraps that backend in
TenantSecretProvider, which scopes the key toNW_{TENANT}_{CONNECTOR}_{KEY}and reads thetenants.yamloverlay before the backend (Tenancy).
Backend variables: Configuration — Secrets Management.
AuthProvider¶
node_wire_runtime.auth.base.AuthProvider. It turns secrets into request credentials: headers, a
query parameter, or a vendor client. The connector's auth: block in connectors.yaml selects the
provider, and auth_schemes: adds named extras. Connectors call await self.get_auth_headers().
Provider types and fields: Build a connector — Authentication.
ErrorMapper¶
node_wire_runtime.errors.ErrorMapper. It maps an exception to (ErrorCategory, error_code) by walking
the exception's MRO for the closest match, within the raising connector's own error_map only.
Runtime-owned exceptions (PolicyDenied, TenantMismatchError) are registered globally. Writing
an error_map: Build a connector.