[AVAILABLE] as a library,
packages/connector-sdk, [AVAILABLE] wired
into the default server for HubSpot, GitHub, Paytm, and Slack, each
registering only when its own credentials are configured. Adding another
connector in code is a code change, see the warning below. To connect a system
you run an HTTPS endpoint for, with no code change, register an external
connector instead: Connect any external
system.Examples below are TypeScript. Building a connector from Python instead? The
identical contract is published as
parmana-connector-sdk on
PyPI — see Connector SDK
(Python).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
This step is internal, not somethingnpm install @parmana/connector-sdk alone gives you.
@parmana/connector-sdk lets you build and test a Connector in isolation. Wiring a built
connector into a live Parmana deployment happens through GatewayConnectorRegistry
(@parmana/execution-gateway) — a private, unpublished package internal to this repository,
the same way packages/api/src/bootstrap/* registers HubSpot, GitHub, Paytm, and Slack
today. If you’re building a connector against the published @parmana/connector-sdk for your
own deployment, this section shows the shape of that internal step, not an API surface you can
call from outside this repository.
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
Razorpay was deliberately removed from the codebase in full. The lessons below
are kept as a historical, worked example (a second real connector, built after
HubSpot’s own lessons already existed to apply), not a claim that Razorpay is
still registered anywhere in this codebase today.
dealstage, optionally alongside amount, not Contacts, not Companies, not
delete. Widen scope in a later milestone, once the first one is fully proven, see
HubSpot’s own [ROADMAP] section on its page.
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. The HubSpot connector refuses its own placeholder token againsthttps://api.hubapi.comfrom its first commit, see HubSpot andpackages/execution-gateway/src/connector-execution/GatewayHubSpotAdapter.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.
Require a signed human approval in every approve rule, reads included. No AI agent action
is authorized without one. PolicyValidator refuses to load a policy whose approve rule does
not require a fact from approvalSignals with is_true at its top level, so a new connector’s
policy must declare one, with resourceId naming what the approval covers (the Intent’s
target, or a path such as parameters.dealId) and, for an amount, value. Give a read its
own policy, like github-pr-read and hubspot-deal-read, so its approval names only what is
read. See Human approval.
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.
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 answers503 CONNECTOR_NOT_REGISTERED. 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.