Skip to main content
[AVAILABLE] as a library, packages/connector-sdk, [AVAILABLE] wired into the default server for Razorpay and HubSpot specifically, a code change for anything beyond those two today, see the warning below.

Connector responsibilities, and what a Connector must never do

A Connector validates a request, executes using an already-resolved credential, and returns a response with a fixed, predictable shape. A Connector never evaluates policy, authorizes execution, interprets AI output, makes a business decision, or resolves a credential itself, credential resolution happens exclusively inside the Execution Gateway, before a Connector is ever called.

Capabilities are namespaced verbs

ConnectorCapability is a plain string, validated eagerly by connectorCapabilities() against namespace:verb (e.g. http:get, crm:read, payments:refund), a malformed capability throws at connector construction, not at execution time. By convention, ExecutableContent.action is the capability string; this is what execution-control’s DefaultConnectorPolicy already checks unchanged.

Registering a connector

registry implements execution-control’s ConnectorRegistry interface (get()), so it plugs directly into ExecutionControlService exactly like InMemoryConnectorRegistry.

Testing your connector hermetically

Use MockConnector to test anything upstream of your connector (policy wiring, routing) without a real target system:
Real output, checked directly against this exact code:
To test failure handling, use script: { failWith: new Error("upstream unavailable") }. mock.invocations records every ConnectorRequest the connector received, so tests can assert exactly what was executed.

Version and health checks fail closed

SdkConnectorExecutor rejects execution, before invoking your connector, if:
  • an expectedVersion was configured at registration and the connector’s own metadata.version doesn’t match (guards a rolling deployment that swapped connector builds underneath a pinned expectation), or
  • metadata.health.status === "unavailable".

What every connector’s evidence looks like

Every execution produces a ConnectorEvidence object, connector ID, version, capability, a sanitized endpoint (credentials and query parameters stripped), the credential provider’s ID, redacted request/response summaries, timestamps, and a hash computed by the existing TrustRecordHasher, attached at ExecutionResult.metadata.connector. See Execution Trust Record for how this reaches the Trust Record without any change to its schema or hashing pipeline.

What reaching the default server actually requires

This is the part that’s easy to assume is configuration and isn’t. Update (Phase 2A), historical record: the default production server used to register a connector (vendor-payment) unconditionally, including in production; that was a trust gap (a request would get a genuinely signed success Trust Record with no real vendor ever contacted) and was closed by gating it to NODE_ENV=test only. Final update (2026-08-09): vendor-payment was subsequently removed from the repository entirely — createVendorPaymentConnector.ts and its dedicated credential provider no longer exist in any form — rather than kept as a gated reference example. See docs/VERIFICATION-GAPS.md G-27 in the source repository. The real, current registration pattern, copied verbatim from source:
Adding a connector to the running server means editing two files: createConnectorRegistry.ts (register it, following the shape above — return undefined in production until a real implementation exists, exactly like createRazorpayCredentialProvider.ts/ createHubSpotCredentialProvider.ts already do) and createConnectorAuthenticator.ts (trust its identity, add an entry to its hardcoded list). Not createConnectorRoute.ts — despite its name, that file’s route() function is dead code in the current production wiring (createExecutionGateway.ts always supplies executionControl.service, which makes the only branch that calls route() unreachable); editing it has no effect on which connector actually handles a request. There is no dynamic registration path, no environment variable that adds a connector, this is a code change today. Razorpay and HubSpot are the two connectors actually reachable in the default production server, see Razorpay and HubSpot for those connectors specifically. See the Phase 2A production-connectors report for the vendor-payment before/after this superseded.

The pattern, proven twice: Razorpay and HubSpot

Both connectors implement everything above, but the two builds themselves are the more useful lesson: Razorpay learned each point below the hard way, on a running connector, after the fact. HubSpot applied all of it from its first version instead. Read this before starting a third connector. Scope to one narrow action first. Don’t build a connector for a whole external API surface. Razorpay started with refund creation only, not payouts, not subscriptions, not the rest of the payments API. HubSpot started with one property update on one object type, a Deal’s dealstage, optionally alongside amount, not Contacts, not Companies, not delete. Widen scope in a later milestone, once the first one is fully proven, see each connector’s own [FUTURE] section on its page (Razorpay, HubSpot). Two structural guards belong in a connector’s first version, not a follow-up fix.
  • Refuse a placeholder test credential against the real endpoint, before any network call. Don’t rely on the vendor happening to reject it, that’s an accident of their behavior, not something this codebase controls. RazorpayConnector didn’t have this check originally, it survived only because Razorpay’s real API happened to reject an unrecognized key — a defense-in-depth fix was added after the fact. HubSpotConnector refuses its own placeholder token against https://api.hubapi.com from its first commit, see HubSpot and packages/connector-hubspot/src/HubSpotConnector.ts.
  • Read the documented test-credential environment variable name directly, never through a bridge variable. createRazorpayCredentialProvider.ts’s test-mode branch originally read a word-order-swapped bridge variable instead of the name .env.example actually documented, fixed only after it drifted out of sync. createHubSpotCredentialProvider.ts reads TEST_HUBSPOT_PRIVATE_APP_TOKEN, the exact documented name, with no bridge to get wrong.
Apply boundSignals hardening from the policy’s first version, don’t wait for an adversarial-testing session to find the gap. razorpay-refund/1.0.0 didn’t declare boundSignals until a live demonstration showed a caller could declare small, verified signals while intent executed something else, closed in an adversarial-testing hardening session. hubspot-deal-update/1.0.0 declares boundSignals binding proposedDealStage/proposedAmount to their intent.parameters fields from its first commit, closing the same class of gap before any live demonstration of it existed. See Policies and the decision. Test order, every time:
  1. Hermetic first. The full authorize → verify → execute → confirm chain against a mock server, zero network calls. Both connectors’ unit suites run this way on every npm test.
  2. Policy-denial-makes-zero-calls, proven at two layers. A direct state check against the mock server (nothing changed), and a fetch spy at the HTTP boundary (literally zero calls reached the mock or real endpoint). See HubSpot for what that looks like in a captured response.
  3. A gated live suite last, behind an ALLOW_LIVE_<CONNECTOR>=1 flag checked alongside a real test credential, skipped by default so a routine npm test never makes a live call by accident.
Prefer a non-destructive live-test action where the target system allows one. Read live, apply a small reversible change, verify independently, revert, in the same run. HubSpot’s live suite nudges a real test deal’s amount by a small within-threshold delta and reverts it before finishing, safe to rerun indefinitely. Razorpay’s refund is irreversible by construction, its captured test payment’s refundable remainder depletes by a fixed amount on every live run — a real operational cost of testing against a real endpoint. Disclose what actually happened, including when the mechanism catches your own mistake. HubSpot’s first live amount-change test run failed with a policy denial the test author didn’t expect, because the test’s own signals payload omitted proposedAmount, SignalIntentBinder correctly rejected the mismatch before the intended amount-threshold rule ever ran. That’s not a connector defect, it’s the binding working exactly as designed, against a test’s own mistake this time instead of a caller’s, and it’s written up that way in docs/CLAIMS.md §3.10 rather than quietly fixed and left undocumented.

Troubleshoot

  • does not declare capability "..." thrown by MockConnector or your own connector. request.capability didn’t match anything in connectorCapabilities([...]). Check for a typo, capability strings are exact, case-sensitive matches.
  • SdkConnectorExecutor rejects with a version mismatch. An expectedVersion was configured at registration and your connector’s metadata.version doesn’t match, this guards a rolling deployment that swapped connector builds underneath a pinned expectation.
  • Registered your connector but POST /execute still says “No connector registered for action.” Registration in a ConnectorSdkRegistry you constructed yourself doesn’t reach the running default server, that server builds its own registry in createConnectorRegistry.ts, see the Warning above.

Next

Issue and verify session credentials

What your connector’s context.credential actually is, and its lifecycle.

Credential isolation

The concept this guide’s credential handling exercises.