@nightowlsdev/connectors
Adapter/ToolingDeclare an external service's actions once, materialize them into approval-gated, output-fenced tools, plus event triggers and out-of-band human-in-the-loop over Slack/email.
What it does
@nightowlsdev/connectors is the connector seam: `defineConnector` declares a provider's actions (agent-callable tools) and events as plain validated data, and `materializeConnectors(connectors, backend)` turns them into `SwarmTool`s for `defineSwarm({ connectorTools })` (root + sub-agents), approval-gated (SP5), with credentials resolved at execute time per tenant and untrusted output fenced before it reaches the model. Backends are pluggable: `staticBackend` (env/config creds, zero infra, the demo default) and `nangoBackend` (per-org OAuth over Nango, platform). Ships the Slack, Linear, and Email channels, plus the FR-058 Google SEO family, `googleSearchConsoleConnector` (Search Analytics + sitemaps + a quota-gated URL inspection) and `ga4Connector` (the GA4 Data API `run_report`), authed by the `googleServiceAccountAuth` RS256 JWT minter and connectable in production via `googleNangoIntegration`. Those ride three additive, generic declarative-op primitives every backend executes identically, so a def stays portable: `paginate` (offset pagination that loops the op body-side until an empty page or `maxPages`), `preflight` (a pre-HTTP short-circuit a quota refusal rides, zero HTTP), and `authHeaderAsync` (an async, cached, 401-invalidating auth closure). Beyond request/response it covers the two ambient capabilities: TRIGGERS, `handleTriggerEvent` verifies a provider webhook signature, dedupes, normalizes, and enqueues one durable run per subscription (identity is server-side, the event payload fenced untrusted); and OUT-OF-BAND HITL, deliver an approval ask to Slack/email and correlate the untrusted reply back to its run, executing the approved action exactly once (dedupe → authz → answered-check → durable resume; the ENGINE owns the answer-once CAS and consumes it once inside `resume`, so no upstream layer may pre-consume it; and the transport dedupe is FINALIZED only on a final classification — a retryable refusal, an uncoded throw or a transient fault RELEASES it via the host-supplied `releaseInboundOnce`, so the provider's redelivery is a real retry rather than an absorbed duplicate). CN8 hardening adds a per-subscription rate limit, an approval-audit seam (who/when/what), and typed connection errors that surface as the core `swarm.connection_error` event so the UI can prompt re-auth. Engine-wall clean, depends only on `@nightowlsdev/core` + `zod`, no Mastra types in the public surface.
Install
pnpm add @nightowlsdev/connectorsKey exports
- defineConnector
- materializeConnectors
- staticBackend / nangoBackend
- staticIntegrationProvider (FR-043: env connections on the IntegrationProvider seam)
- slackConnector / linearConnector / emailConnector
- googleSearchConsoleConnector / ga4Connector (FR-058: the Google SEO family)
- googleServiceAccountAuth (RS256 JWT minter) / googleNangoIntegration
- op primitives: paginate (offset) / preflight (pre-HTTP short-circuit) / authHeaderAsync (async auth)
- handleTriggerEvent (webhook intake) / verifySlackSignature / verifyLinearSignature
- slackQuestionDelivery / emailQuestionDelivery / handleInboundReply (out-of-band HITL)
- ConnectorConnectionError / isConnectionError / asConnectionError (CN8)
Usage
import { z } from "zod";
import { defineConnector, staticBackend, materializeConnectors } from "@nightowlsdev/connectors";
import { defineSwarm } from "@nightowlsdev/core";
// 1. Declare a provider's actions (validated data). Side-effecting actions default to needsApproval.
const slack = defineConnector({
provider: "slack",
actions: [
{ name: "slack.post_message", inputSchema: z.object({ channel: z.string(), text: z.string() }),
op: { method: "POST", path: "/chat.postMessage" } },
],
});
// 2. Pick a backend (staticBackend = env creds, zero infra; nangoBackend = per-org OAuth).
const backend = staticBackend({
connections: { slack: { baseUrl: "https://slack.com/api", secretRef: "slack-bot-token" } },
secrets, // a SecretResolver, resolved at execute time scoped to the run's tenant
});
// 3. Grant the materialized tools to your agents (approval-gated + output-fenced).
const swarm = defineSwarm({ agents, connectorTools: materializeConnectors([slack], backend) });
// FR-058 also ships the Google SEO family. Each factory returns { connector, connection, scope, provider }:
// the backend-agnostic connector materializes over staticBackend AND nangoBackend, and connection is a
// staticBackend connection pre-wired with service-account auth. url_inspection materializes ONLY with a quota.
import { googleSearchConsoleConnector, ga4Connector } from "@nightowlsdev/connectors";
const gsc = googleSearchConsoleConnector({ siteUrl: "https://example.com/", credentialRef: "gsc-sa" });
const ga4 = ga4Connector({ propertyId: "properties/123", credentialRef: "ga4-sa" });
// Also: handleTriggerEvent (webhook → run), slackQuestionDelivery/handleInboundReply (out-of-band HITL),
// and the CN8 hardening (per-subscription rateLimit, approval audit, swarm.connection_error).What it provides
connectors is the connector seam: `defineConnector` declares an external service's actions (agent-callable tools) and events as plain validated data, and `materializeConnectors(connectors, backend)` turns them into SwarmTools for defineSwarm({ connectorTools }) — approval-gated, with per-tenant credentials resolved at execute time and untrusted output fenced before it reaches the model. Backends are pluggable (staticBackend for env/config creds; nangoBackend for per-org OAuth), so the same def works with no agent-code change when the backend swaps. Beyond request/response it also covers the two ambient capabilities: event TRIGGERS (verify a webhook signature, dedupe, normalize, enqueue one durable run per subscription) and out-of-band HITL (deliver an approval ask to Slack/email and correlate the untrusted reply back to its run, resuming exactly once).
When to use it
- You want to grant an agent a real external capability — post to Slack, file a Linear issue, send email, query an HTTP API — as an approval-gated, output-fenced tool.
- You need one connector definition to work across credential backends: env vars in dev (staticBackend), per-org OAuth in production (nangoBackend or an IntegrationProvider) with no change to the agent.
- You want provider webhooks to start durable agent runs, or an approval ask delivered to Slack/email and answered out-of-band.
- You want a portable, HTTP-shaped action DSL (paginate, preflight, async auth) executed identically by every backend — the shipped Slack, Linear, Email, and Google SEO (Search Console + GA4) families are built on it.
When not to
- The capability already lives behind an MCP server — use @nightowlsdev/mcp to bridge it, rather than re-declaring its actions here.
- You want a managed catalog of 1000+ apps' tools instead of hand-authoring defs — reach for the @nightowlsdev/integration-composio backend on the IntegrationProvider seam this package defines.
- It's a purely local, in-process tool with no external service, credential, or fence concern — a plain defineTool in @nightowlsdev/core is lighter.
Alternatives
- @nightowlsdev/mcpThe external capability is exposed over the MCP protocol. mcp wraps an MCP client into the same approval-gated, fenced SwarmTools without you declaring each action's HTTP shape.
- @nightowlsdev/integration-composioYou want breadth over authorship: a catalog-owning backend where Composio provides the tools, runs the call, and holds the credentials. It plugs into the IntegrationProvider seam defined here.
- @nightowlsdev/integration-nativeYou want the open default: self-run OAuth with credentials on your own infrastructure, driving your hand-authored defineConnector defs. It implements the IntegrationProvider seam from this package.
Strengths
- Declarative and portable: an action is validated data with an HTTP op; staticBackend, nangoBackend, and the Nango provider all execute paginate/preflight/authHeaderAsync identically, so a def never special-cases its backend.
- Safe by default: side-effecting actions default to needsApproval (SP5 gate), credentials resolve at execute time scoped to the tenant, and untrusted output is fenced before it reaches the model.
- Covers the ambient capabilities most frameworks omit: signed-and-deduped webhook intake that enqueues durable runs, and out-of-band Slack/email HITL that executes an approved action exactly once (dedupe → authz → answered-check → durable resume; the ENGINE owns the answer-once CAS and consumes it once inside resume; the transport dedupe is finalized only on a FINAL classification, and releases on anything retryable via `releaseInboundOnce`).
- Typed connection errors surface as the core swarm.connection_error event, so the UI can prompt re-auth instead of silently failing; CN8 adds per-subscription rate limits and an approval audit seam.
- Engine-wall clean: depends only on @nightowlsdev/core + zod, with no Mastra types in its public surface.
Limits & trade-offs
- staticBackend is a single credential per connection with zero lifecycle: startConnect throws (there is no OAuth dance for an env var) and revoke is not real — per-org connect/revoke needs nangoBackend or an IntegrationProvider.
- The op DSL is HTTP-shaped and offset-pagination-only in v1 (token/cursor pagination and query-string placement are future additive members); a non-HTTP or streaming protocol doesn't fit the op model.
- The ambient capabilities are wiring, not turnkey: triggers and out-of-band HITL require you to supply the queue, the dedupe store, the durable resume, and the delivery channel — this package holds no timer or queue of its own.
- The paginate guarantee holds only for defs built THROUGH defineConnector — a raw structural ConnectorDef bypasses its validation (a validated-def seam for raw defs is a follow-on).
How it works
defineConnector validates a provider + its actions (each an input schema plus a declarative op, or a preflight short-circuit) and optional events into a plain ConnectorDef. A backend's materialize turns each action into a SwarmTool: at execute time it resolves the connection's credential through your SecretResolver (scoped by the run's tenant, and per executing agent under FR-058), runs the op — walking paginate offset pages body-side, honoring a preflight that can refuse before any HTTP, refreshing an authHeaderAsync token reactively on a 401 — then fences the untrusted result before returning it. materializeConnectors(connectors, backend) bundles that into the (ctx) => SwarmTool[] resolver defineSwarm({ connectorTools }) consumes for root and sub-agents. On the ambient side, handleTriggerEvent verifies the provider's webhook signature, dedupes per (provider, externalId), normalizes via the event spec, and enqueues one durable run per subscription with the payload fenced; slackQuestionDelivery/emailQuestionDelivery push an approval ask to a channel and handleInboundReply correlates the untrusted reply back to its followupId and resumes exactly once.
Examples
Declare a connector, pick a backend, grant the tools
Side-effecting actions default to needsApproval; read-only ones set needsApproval:false. materializeConnectors feeds connectorTools.
import { z } from "zod";
import { defineConnector, staticBackend, materializeConnectors } from "@nightowlsdev/connectors";
import { defineSwarm } from "@nightowlsdev/core";
const slack = defineConnector({
provider: "slack",
actions: [
{ name: "slack.post_message", inputSchema: z.object({ channel: z.string(), text: z.string() }),
op: { method: "POST", path: "/chat.postMessage" } }, // side-effecting → approval-gated
{ name: "slack.search", inputSchema: z.object({ q: z.string() }),
needsApproval: false, op: { method: "GET", path: "/search.messages" } }, // read-only
],
});
// staticBackend = env/config creds, zero infra (nangoBackend = per-org OAuth).
const backend = staticBackend({
connections: { slack: { baseUrl: "https://slack.com/api", secretRef: "slack-bot-token" } },
secrets, // a SecretResolver, resolved at execute time scoped to the run's tenant
});
const swarm = defineSwarm({ agents, connectorTools: materializeConnectors([slack], backend) });Turn a provider webhook into a durable run
Signature verify → dedupe → normalize → enqueue one durable run per subscription. Identity is server-side; the payload is fenced untrusted.
import { handleTriggerEvent, slackConnector } from "@nightowlsdev/connectors";
// In your webhook route handler:
const result = await handleTriggerEvent("slack", rawBody, {
connector: slackConnector, // its event spec normalizes the payload
signingSecret: process.env.SLACK_SIGNING_SECRET, // Slack HMAC verify (signature + timestamp)
signature: headers["x-slack-signature"],
timestamp: headers["x-slack-request-timestamp"],
dedupe, // per (provider, externalId) idempotency store
lookupSubscriptions, // who is subscribed (identity comes from here, not the payload)
enqueue, // enqueues one durable run per subscription
mintRunId: () => crypto.randomUUID(),
});Doing the parts it doesn't support
- Per-org OAuth and revocable connections with the static backendstaticBackend is env-cred-only: startConnect throws and revoke is not real. For a real connect/revoke lifecycle, use nangoBackend, or wire an IntegrationProvider — @nightowlsdev/integration-native (self-run OAuth, your infra) or @nightowlsdev/integration-composio (managed catalog) — via materializeIntegration.
- Cursor/token pagination or a non-HTTP protocolpaginate is offset-only in v1; token/cursor kinds and query-string placement are planned additive members. A capability that isn't request/response HTTP (a streaming or socket protocol) doesn't fit the op DSL — bridge it as an MCP server (@nightowlsdev/mcp) or a plain defineTool instead.
- Scheduling the webhook-less poll or the HITL queueThis package holds no timer or queue. Drive polling and delivery on your own schedule (cron / Trigger.dev via @nightowlsdev/runner-background) and inject the dedupe store, durable enqueue, and resume — the connectors just do the verify/normalize/correlate steps.
Related
- integration-native — The open-default IntegrationProvider backend: self-run OAuth, credentials on your infrastructure, implementing this package's seam.
- integration-composio — The catalog-owning IntegrationProvider backend: 1000+ apps' tools, executed and credentialed by Composio.
- mcp — The sibling tool-source seam for capabilities exposed over the MCP protocol.
- core — SwarmTool, connectorTools, the SecretResolver seam, and the swarm.connection_error event connectors surface.
- agent-seo — Wiring the pre-built SEO crew over the Google Search Console + GA4 connectors this package ships.