Skip to content

guides

Intended Documentation

Connector SDK

Build a fail-closed connector by extending BaseAdapter from @intended/connector-sdk. The base class verifies the Authority Token — signature, tenant, adapter, expiry, single-use nonce — before your action code ever runs.

Connector SDK#

A connector (adapter) is the last line of defense: it is the component that actually performs an approved action against a downstream system, and it refuses to act unless the Authority Token verifies. You build one by extending BaseAdapter from @intended/connector-sdk and implementing a single method, executeAction. The base class does the security-critical work — token verification with kid pinning, tenant/adapter/target binding, expiry, and single-use nonce consumption — and it is fail-closed: if any check fails, your executeAction is never called.

Warning

Do not implement your own execute() or skip the base-class validate(). BaseAdapter.execute() always validates the token first and returns a rejected result on any failure, before touching the downstream system. That is the guarantee the connector model rests on.

Note

This is a different thing from @intended/adapters (the LangChain / Semantic Kernel / CrewAI / AutoGen packages). Those map an agent framework's actions into intents on the way in. The connector SDK described here runs after a decision, verifying the issued token before executing. Don't conflate the two.

The validation pipeline#

When the runtime dispatches an approved action, it hands your adapter a ConnectorEnvelope containing the authorityToken. BaseAdapter.execute() runs this pipeline before any action logic:

Parse the envelope

The envelope is validated against ConnectorEnvelopeSchema (tenantId, correlationId, intentId, action, adapterId, adapterTarget, authorityToken, optional payload / resource).

Decode kid and fetch the tenant public key

The token header is decoded, and the tenant's public key for that kid is fetched (getAuthorityPublicKeyByKid). An unknown kid fails closed.

Verify the Ed25519 signature with the kid pinned

verifyAuthorityDecisionToken checks the signature against the pinned key id.

Bind the claims

Assert the token's tenantId, adapterId, and adapterTarget match the envelope, and that decision === "APPROVED". Mismatches throw TOKEN_TENANT_MISMATCH, TOKEN_ADAPTER_MISMATCH, TOKEN_ADAPTER_TARGET_MISMATCH, or TOKEN_DECISION_INVALID.

Check expiry

A token past expiresAt throws TOKEN_EXPIRED.

Consume the nonce (single use)

consumeAuthorityTokenNonce atomically consumes the per-tenant nonce. A replay whose nonce is already consumed is rejected. Only now is executeAction called.

Connector validation, then execution
The runtime hands an approved envelope to the adapter; BaseAdapter decodes the kid, fetches the tenant key, verifies the Ed25519 signature, binds tenant/adapter/target, checks expiry, consumes the single-use nonce, and only then calls executeAction. Any failure returns a rejected result without touching the downstream system.

Validation runs before execution. Any failed check returns a rejected ExecutionResult and the downstream system is never touched.

Build a connector#

Extend BaseAdapter, provide a _manifest, and implement executeAction. The base class hands you the already-validated claims, so your code only contains system-specific logic.

ts
import {
  BaseAdapter,
  type ConnectorEnvelope,
  type ConnectorManifest,
  type ExecutionResult,
} from "@intended/connector-sdk";
import type { AuthorityDecisionTokenClaims } from "@intended/contracts";

export class GithubActionsAdapter extends BaseAdapter {
  protected readonly _manifest: ConnectorManifest = {
    id: "github-actions-adapter",
    name: "GitHub Actions",
    version: "1.0.0",
    description: "Dispatches GitHub Actions workflows and opens PRs.",
    // Action patterns are {system}.{entity}.{verb}, lowercase + hyphens.
    capabilities: ["github.workflow.dispatch", "github.pull-request.create"],
    supportedAuthMethods: ["github-app", "pat"],
    status: "active",
  };

  // Called ONLY after BaseAdapter has fully validated the token.
  protected async executeAction(
    envelope: ConnectorEnvelope,
    claims: AuthorityDecisionTokenClaims,
  ): Promise<ExecutionResult> {
    // claims.adapterTarget is the verified target, e.g. "acme/platform-api".
    const remote = await this.dispatchWorkflow(envelope.payload, claims.adapterTarget);

    return {
      adapterId: this._manifest.id,
      attempted: true,
      status: "executed",
      reason: "workflow dispatched",
      executionTarget: claims.adapterTarget,
      remoteStatusCode: remote.status,
      validatedClaims: claims,
      data: { runId: remote.runId },
    };
  }

  private async dispatchWorkflow(_payload: unknown, _target: string) {
    // ... your GitHub API call here ...
    return { status: 204, runId: "run_123" };
  }
}

Manifest fields (ConnectorManifestSchema)#

FieldTypeNotes
idstringUnique adapter id; the token's adapterId must equal this.
namestringDisplay name.
versionstringSemantic version.
descriptionstringOne line.
capabilitiesstring[]≥1 action patterns, each {system}.{entity}.{verb} (lowercase, hyphens).
supportedAuthMethodsarraySubset of oauth2, api-token, github-app, pat.
status"active" | "scaffolded"

Envelope fields (ConnectorEnvelopeSchema)#

FieldTypeNotes
tenantIdstringMust match the token.
correlationIduuidTies to the audit chain.
intentIduuidThe originating intent.
actionstringThe action being executed.
adapterIdstringMust equal the manifest id and the token adapterId.
adapterTargetstringMust equal the token adapterTarget.
authorityTokenstringThe Ed25519 JWT from POST /intent.
payload / resourceobjectOptional; your action inputs.

Register the connector#

Register adapters in an AdapterRegistry. The registry refuses to silently overwrite an existing id and is what the execution engine looks up by adapterId.

ts
import { AdapterRegistry } from "@intended/connector-sdk";

const registry = new AdapterRegistry();
registry.register(new GithubActionsAdapter());

// Look up by id (fail closed: get() returns undefined when missing).
const adapter = registry.getOrThrow("github-actions-adapter");
const result = await adapter.execute(envelope);

if (result.status !== "executed") {
  // result.reason is the failure code, e.g. TOKEN_EXPIRED or a nonce-replay reason.
}

AdapterRegistry also exposes get, listIds, listManifests, testConnection(adapterId, tenantId), and testAllConnections(tenantId).

Execution outcomes#

executeAction returns an ExecutionResult. The base class also synthesizes one on a failed validation or a thrown error.

statusWhenMeaning
executedYour executeAction succeeded.The downstream action was performed.
rejectedToken validation failed, or executeAction threw.Nothing was executed; reason carries the code.
skippedYour executeAction chose not to act.A deliberate no-op you returned.

Validation-failure reasons surfaced as reason on a rejected result include TOKEN_TENANT_MISMATCH, TOKEN_ADAPTER_MISMATCH, TOKEN_ADAPTER_TARGET_MISMATCH, TOKEN_DECISION_INVALID, TOKEN_EXPIRED, and the nonce-replay reason returned by consumeAuthorityTokenNonce. A thrown executeAction is caught and returned as rejected rather than propagating — the connector never half-executes on an error.

testConnection

BaseAdapter provides a default testConnection that reports the adapter as reachable. Override it with a real health check against your downstream system:

ts
async testConnection(tenantId: string) {
  const start = Date.now();
  const ok = await this.ping();
  return {
    adapterId: this._manifest.id,
    reachable: ok,
    latencyMs: Date.now() - start,
  };
}

Next steps#

Connector SDK | Intended