[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
UseMockConnector to test anything upstream of your connector (policy wiring, routing)
without a real target system:
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
expectedVersionwas configured at registration and the connector’s ownmetadata.versiondoesn’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 aConnectorEvidence 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
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’sdealstage, 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.
RazorpayConnectordidn’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.HubSpotConnectorrefuses its own placeholder token againsthttps://api.hubapi.comfrom its first commit, see HubSpot andpackages/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.exampleactually documented, fixed only after it drifted out of sync.createHubSpotCredentialProvider.tsreadsTEST_HUBSPOT_PRIVATE_APP_TOKEN, the exact documented name, with no bridge to get wrong.
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:
- 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. - Policy-denial-makes-zero-calls, proven at two layers. A direct state check against
the mock server (nothing changed), and a
fetchspy at the HTTP boundary (literally zero calls reached the mock or real endpoint). See HubSpot for what that looks like in a captured response. - A gated live suite last, behind an
ALLOW_LIVE_<CONNECTOR>=1flag checked alongside a real test credential, skipped by default so a routinenpm testnever makes a live call by accident.
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 byMockConnectoror your own connector.request.capabilitydidn’t match anything inconnectorCapabilities([...]). Check for a typo, capability strings are exact, case-sensitive matches.SdkConnectorExecutorrejects with a version mismatch. AnexpectedVersionwas configured at registration and your connector’smetadata.versiondoesn’t match, this guards a rolling deployment that swapped connector builds underneath a pinned expectation.- Registered your connector but
POST /executestill says “No connector registered for action.” Registration in aConnectorSdkRegistryyou constructed yourself doesn’t reach the running default server, that server builds its own registry increateConnectorRegistry.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.