Build
Connector SDK
A connector teaches Fibric to sense and act on one system. You declare it with three exports, defineConnector, tool, and auth, and the platform gives you the rest: capability binding so operators never depend on your brand, the governed executor for anything that changes the world, and a marketplace listing other tenants can install. First-class TypeScript and Python.
defineConnector()
Declares the connector: its name, the system it speaks to, its auth, and the tools it exposes.
returns ConnectorDeftool()
Declares one capability the connector can perform, its input schema, and whether it changes the world.
returns ToolDefauth.*
Declares how credentials are obtained, resolved from the vault at call time, never hard-coded.
returns AuthSpecdefineConnector #
Every connector is a default export built with defineConnector. The returned ConnectorDef is the whole contract: the platform reads it to register your capabilities, mount your auth in the vault, and publish the listing. Nothing about your connector is hidden in runtime behavior, the def is the source of truth.
interface ConnectorDef {
name: string; // stable slug, e.g. "acme-wms"
summary: string; // one line shown in the marketplace
version?: string; // semver; the CLI stamps it on publish
auth: AuthSpec; // how credentials are obtained
tools: ToolDef[]; // the capabilities this connector exposes
homepage?: string; // vendor docs / status page
scopes?: string[]; // OAuth scopes requested, if any
}
| Field | Required | Notes |
|---|---|---|
name | yes | Lowercase slug, unique within the marketplace. Becomes the install id. |
summary | yes | One sentence. This is the line a reader skims in the marketplace grid. |
auth | yes | An AuthSpec from the auth helper. Resolved per connection at call time. |
tools | yes | At least one ToolDef. Each binds to a capability operators can request. |
version | no | Semver. Omit it and the CLI prompts on publish; pin it for reproducible builds. |
tool #
A tool is one thing the connector can do. It binds to a capability, the verb an operator asks for, not your brand. Two connectors can both provide order.read; an operator that asks for order.read works against either, so swapping a vendor stays a configuration change. The most consequential field is sideEffecting: it decides whether a tool runs inline or routes through the governed executor.
interface ToolDef<I> {
name: string; // unique within the connector
capability: string; // the verb operators bind to, e.g. "order.hold"
input: Schema<I>; // zod (TS) / pydantic (Py); validated before run
sideEffecting?: boolean; // false (default) runs inline; true routes through the executor
idempotent?: boolean; // hint: is a retry of the same args safe to coalesce
summary?: string; // shown on the capability in the marketplace
run(args: { input: I; ctx: ToolCtx }): Promise<unknown>;
}
| Field | Required | Notes |
|---|---|---|
name | yes | Unique within the connector. Identifies the tool in receipts and dry-runs. |
capability | yes | The capability this tool fulfills. Operators request the capability, never the tool. |
input | yes | A schema. Input is validated before run is ever called; bad input never reaches your code. |
sideEffecting | no | Defaults to false. Set true for anything that changes the outside world. See below. |
idempotent | no | Tells the executor a retry with the same idempotency key is safe to coalesce. |
run | yes | The body. Gets validated input and a ctx with the resolved HTTP client and credentials. |
sideEffecting routing #
This one flag is what makes Fibric governable. A read tool senses the world and can run the moment an operator asks. A side-effecting tool changes the world, so it is never called directly. It becomes a step in a proposed ExecutionPlan that the deterministic executor disposes of, under your trust policy, single-flight per entity, and idempotency keys.
sideEffecting: false
Runs inline. No plan, no policy gate, because nothing in the outside world changes. Reads are how the operator perceives: open orders, a thermostat reading, an inbox.
sideEffecting: true
Becomes a plan step. The executor checks it against your policy, acquires single-flight, applies the idempotency key, runs the tool, and writes a receipt. Fail-closed: if policy is silent, the step does not run.
If a tool writes, posts, refunds, holds, emails, opens a door, or moves anything in a system of record, it is side-effecting. The cost of marking a read as side-effecting is a little latency; the cost of marking a write as a read is an ungoverned action with no receipt. Choose the safe failure.
A full connector #
Here is a complete, publishable connector for a warehouse management system. It exposes one read capability (order.read) and one side-effecting capability (order.hold). An operator that wants to hold slipping orders binds to those capabilities, never to acme-wms itself.
TypeScript
import { defineConnector, tool, auth } from "@fibric/sdk";
import { z } from "zod";
export default defineConnector({
name: "acme-wms",
summary: "Acme warehouse management system",
version: "1.2.0",
// credentials are resolved from the vault per connection, never hard-coded
auth: auth.apiKey({ header: "X-Acme-Key", secret: "ACME_API_KEY" }),
tools: [
// ---- READ: senses the world, runs inline, no plan ----
tool({
name: "list_open_orders",
capability: "order.read",
summary: "List open orders, newest first",
input: z.object({ since: z.string().datetime() }),
async run({ input, ctx }) {
const { data } = await ctx.http.get("/orders", {
params: { status: "open", since: input.since },
});
return data.orders;
},
}),
// ---- WRITE: side-effecting, routes through the governed executor ----
tool({
name: "hold_order",
capability: "order.hold",
summary: "Place a hold on one order so it cannot ship",
sideEffecting: true,
idempotent: true,
input: z.object({
orderId: z.string(),
reason: z.string().min(8),
}),
async run({ input, ctx }) {
const { data } = await ctx.http.post(
`/orders/${input.orderId}/hold`,
{ reason: input.reason },
);
return { held: true, orderId: input.orderId, at: data.heldAt };
},
}),
],
});
Python
from fibric import define_connector, auth
from pydantic import BaseModel, Field
class ListOpenOrders(BaseModel):
since: str
class HoldOrder(BaseModel):
order_id: str
reason: str = Field(min_length=8)
connector = define_connector(
name="acme-wms",
summary="Acme warehouse management system",
version="1.2.0",
# credentials resolved from the vault per connection, never hard-coded
auth=auth.api_key(header="X-Acme-Key", secret="ACME_API_KEY"),
)
# ---- READ: senses the world, runs inline, no plan ----
@connector.tool(capability="order.read", summary="List open orders, newest first")
def list_open_orders(input: ListOpenOrders, ctx) -> list[dict]:
resp = ctx.http.get("/orders", params={"status": "open", "since": input.since})
return resp.json()["orders"]
# ---- WRITE: side-effecting, routes through the governed executor ----
@connector.tool(
capability="order.hold",
summary="Place a hold on one order so it cannot ship",
side_effecting=True,
idempotent=True,
)
def hold_order(input: HoldOrder, ctx) -> dict:
resp = ctx.http.post(
f"/orders/{input.order_id}/hold",
json={"reason": input.reason},
)
return {"held": True, "order_id": input.order_id, "at": resp.json()["heldAt"]}
TypeScript uses zod, Python uses pydantic, but the ConnectorDef and ToolDef they produce are identical on the wire. Pick the language your team already runs; an operator cannot tell which one built the connector it is calling.
auth #
The auth helper declares how a connection gets its credentials. You never see a secret value in your code, you name the slot, and the platform resolves it from the per-tenant vault at call time and injects it into ctx.http. The same connector code serves every tenant that installs it; their credentials never cross the wall.
// 1. API key in a header
auth.apiKey({ header: "X-Acme-Key", secret: "ACME_API_KEY" })
// 2. Bearer token
auth.bearer({ secret: "ACME_TOKEN" })
// 3. Basic auth
auth.basic({ user: "ACME_USER", pass: "ACME_PASS" })
// 4. OAuth 2.0 (the platform runs the dance and refreshes for you)
auth.oauth2({
authorizeUrl: "https://acme.example/oauth/authorize",
tokenUrl: "https://acme.example/oauth/token",
scopes: ["orders.read", "orders.write"],
})
| Helper | Use when |
|---|---|
auth.apiKey | The system takes a static key in a named header. |
auth.bearer | A single bearer token in Authorization. |
auth.basic | HTTP basic, username and password. |
auth.oauth2 | OAuth 2.0. The platform stores, refreshes, and injects the token; you never touch it. |
auth.none | A public system with no credentials, rare, but supported for open data feeds. |
Publish to the marketplace #
A connector becomes installable the moment you publish it. The CLI validates it against the marketplace contract, stamps a semver, and lists it so any tenant can install it and create their own connection. Operators in that tenant then bind to its capabilities. The platform stays free; connectors are the add-ons.
# 1. validate against the marketplace contract
fibric connector validate ./connectors/acme-wms
# 2. dry-run a tool against a real connection, nothing is published
fibric connector test acme-wms list_open_orders --input '{"since":"2026-06-01T00:00:00Z"}'
# 3. publish a versioned, immutable build
fibric connector publish ./connectors/acme-wms --version 1.2.0
# 4. confirm it is live in the marketplace
fibric connector show acme-wms
Validation enforces the contract every listed connector honors:
- Every tool binds to a known capability, and capability names follow the
noun.verbshape. - Every side-effecting tool declares an input schema strict enough to validate before it runs.
- Auth names only vault slots; no secret literal appears anywhere in the connector.
- The version is semver, immutable once published; a fix is a new version, never an overwrite.
- The summary is present and a single sentence, so the marketplace grid stays scannable.
Once 1.2.0 is published it can never change, so a tenant pinned to it can trust it will behave the same tomorrow. Ship a fix as 1.2.1; tenants upgrade on their own schedule. Deprecate an old line with fibric connector deprecate acme-wms@1.1.x.
For the operator side of this loop, how a proposed ExecutionPlan is disposed of and turned into receipts, see Operators and Governance & trust. To drive publishing and connections from scripts, see the CLI reference.