configure
v1.2.0
Published
Identity layer SDK for AI agents
Readme
Configure TypeScript SDK
Configure is a personalization SDK for agents. It gives your agent user-approved context from other agents, app contexts, preferences, memories, and more over API, MCP, or tool calls.
Most projects should integrate Configure into an existing agent: add hosted Link or a Personalization entry in the browser, send the configure:linked token to your backend, expose Configure tools, route configure_* calls, and commit after read-backed turns. Use the packaged template only when you want a fresh Configure-backed chat shell.
Install and setup:
npx configure setup
npm install configureThen prove the credentials work and generate the callback:
npx configure verify
npx configure add callback --framework next
npx configure add origin https://yourapp.com/auth/configure/callbackverify completes a real sign-in, exchanges the code, and reads a profile (--offline checks credentials and callback registration without a browser). add callback takes next, express, or vite and writes the callback route, the server-side exchange, and the sign-in button, keeping the client secret off the browser. The generated callback exchanges the single-use code once, recovers the PKCE verifier when state is missing, and hands off to the opener in a popup instead of navigating. add origin registers a deployed callback on the same client before you ship: it opens the dashboard to confirm the exact client and callback, since an sk_ key cannot change an OAuth client, and registration is additive so CONFIGURE_OAUTH_CLIENT_ID never changes.
Choose For my users when adding Configure to a product you already ship. Choose your existing agent handle at setup; register a new handle only for a fresh agent shell. Setup opens Configure in your browser, creates production credentials, and writes CONFIGURE_API_KEY, CONFIGURE_PUBLISHABLE_KEY, CONFIGURE_AGENT, CONFIGURE_OAUTH_CLIENT_ID, and CONFIGURE_OAUTH_CLIENT_SECRET to .env. If all five already exist, reuse them and skip setup; if the OAuth pair is missing, rerun setup to add it.
The public server-side SDK shape is:
import { Configure } from "configure";
const configure = new Configure({
apiKey: process.env.CONFIGURE_API_KEY,
agent: process.env.CONFIGURE_AGENT,
});
const profile = configure.profile({ token });
// Patch your EXISTING model loop. Expose Configure tools alongside your own,
// then route configure_* calls to Configure. profile.tools() is Anthropic-native
// ({ name, description, input_schema }); wrap with toOpenAIFunctions() for OpenAI.
// executeTool accepts { name, arguments } or { name, input }.
const tools = [
...yourTools,
...profile.tools(),
];
const dispatch = (toolCall) =>
toolCall.name.startsWith("configure_")
? profile.executeTool(toolCall)
: executeYourTool(toolCall);
const response = await runYourModelLoop({ messages, tools, dispatch });
// Commit bounded turn evidence after a read-backed turn.
await profile.commit({ messages, response, memories: response.memoryCandidates });Concrete OpenAI and Anthropic loops: https://docs.configure.dev/guides/tool-calling.
Use Configure tools as the normal personalization path. Configure does not own or mutate your system prompt. If your app deliberately supports a preloaded approved context slot, you can read explicit sections and format them there, while keeping Configure tools available for model-driven reads and searches:
const read = await profile.read({
sections: ["identity", "preferences", "summary"],
});
const approvedContext = read.profile.format({ guidelines: false });For concrete or source-specific questions, let the model call configure_profile_search, for example source: "chatgpt", query: "*".
For tight prompt budgets, choose narrower optional sections and keep profile.search() / configure_profile_search available for concrete follow-up retrieval.
The stable default behavior remains tool-driven: expose profile.tools() and route configure_* calls through profile.executeTool(). When your hosted/product surface requested connector or action capabilities and the app supports them, add those tools explicitly. Tool visibility means app capability, not user authorization; profile.executeTool() and the backend still fail closed when the user is unlinked, disconnected, underscoped, missing permissions, unapproved, or needs recovery:
const tools = [
...yourTools,
...profile.tools({
connectors: ["gmail", "calendar", "drive", "notion"],
actions: ["email.send", "calendar.create_event"],
}),
];When a connector or action is unavailable, catch the structured Configure failure and send the user through the hosted connect, reconnect, permissions, or approval surface instead of inventing URLs or permission logic in the model. The model may see supported write capabilities, but final execution still requires a clear user request plus the relevant connection, permission, scope, approval, and runtime policy.
Verify the wiring without a model — execute the read tool directly. When the model calls configure_profile_read in your loop, that is the same dispatch:
const result = await profile.executeTool({
name: "configure_profile_read",
arguments: { sections: ["identity", "summary", "preferences", "imports"] },
});For app-local users that have not linked a Configure identity yet, pass your stable user identifier as externalId:
const profile = configure.profile({ externalId: "customer-123" });Message-agent SSO
Every registered agent has one hosted sign-in URL. Send the sign_in_url returned by the agent API, or get the same value from configure.auth.signInUrl():
await message.reply(`Connect your profile: ${configure.auth.signInUrl()}`);Configure resolves the browser credential, branding, phone verification, consent, and connectors server-side. Multiple Gmail accounts work by default. New integrations do not pass a publishable key, delivery mode, or phone number in the URL.
For message channels that provide provider-signed sender binding or need a thread-specific completion webhook, use the advanced SDK helpers below.
const subjectKey = message.sender?.id || space.id;
const agentPhone = await smsProvider.currentPhone();
await configure.auth.registerMessageLine({
channel: "sms",
phone: agentPhone,
label: "Primary SMS line",
metadata: { provider: "your-sms-provider" },
});
const handoff = await configure.auth.createMessageSignInUrl({
reason: "signin",
channel: "sms",
subject: {
key: subjectKey,
externalId: `sms:${subjectKey}`,
senderId: message.sender?.id,
},
thread: {
key: space.id,
spaceId: space.id,
messageId: message.id,
},
messageSenderProof: providerSignedMessageSenderProof,
agentPhone,
messageBody: "done",
connectors: ["gmail", "calendar"],
idempotencyKey: `${space.id}:${message.id}:signin`,
});
await message.reply(`Connect your profile: ${handoff.url}`);This advanced path binds the provider's current return line and signed sender evidence. Register agentPhone before generating a managed URL. createMessageSignInUrl() falls back to the canonical hosted URL when Configure cannot verify sender proof. Application code always sends the returned URL unchanged.
On each inbound message, resolve the best Configure identity. A linked user returns a token; everyone else keeps working with your app-local externalId:
const subjectKey = message.sender?.id || space.id;
const stored = await store.get(subjectKey);
const identity = await configure.auth.resolveMessageIdentity({
externalId: `sms:${subjectKey}`,
token: stored?.configureToken,
phoneCandidates: [
message.sender?.phone,
message.sender?.address,
message.sender?.id,
space.phone,
].filter((value): value is string => Boolean(value)),
});
if (identity.token && identity.token !== stored?.configureToken) {
await store.save(subjectKey, {
configureToken: identity.token,
configureUserId: identity.userId,
});
}For existing Better Auth apps, pass configureBetterAuthOAuthProvider() into Better Auth's Generic OAuth plugin:
import { configureBetterAuthOAuthProvider } from "configure";Use configure.auth.signInUrl({ returnTo, state, ... }), auth.allowSignInReturnTo(), and auth.exchangeSignInCode() when Configure is an account connection inside an already signed-in app session.
baseUrl is only for internal staging/local development and advanced deployments; it should not appear in the normal production path.
Default model tools are only:
configure_profile_readconfigure_profile_searchconfigure_profile_remember
Everything else is explicit app capability. Connector tools, action tools, utility web tools, raw file tools, and hosted UI tool helpers are optional surfaces; expose only the categories your runtime actually supports, then let profile.executeTool() enforce user state and permissions.
profile.search() returns compact attributed hits by default. Compact hits omit raw CFS paths and provenance; pass detail: "full" when an inspector or admin flow needs safe metadata such as path, markers, provenance, and updated_at.
Use sections: ["imports"] or profile.search({ query: "*", source: "chatgpt" }) for user-directed ChatGPT/Claude/etc. imported memories. Use integrations for connected tools such as Gmail, Outlook, Calendar, Drive, and Notion.
Outlook-aware runtimes opt in with connectors: ["outlook"]. This adds configure_email_search; omitting provider and account searches every permitted Gmail and Outlook account. Gmail-only runtimes keep the existing configure_gmail_search and email-send schema.
Connector and action tools are enabled explicitly when the hosted/product surface requested those capabilities and the app supports them:
profile.tools({
connectors: ["gmail", "calendar", "drive", "notion"],
actions: ["email.send", "calendar.create_event"],
});You can also enable only the action subset your app supports. Let execution enforce linked state, connector state, permissions, scopes, and any required approval flow:
profile.tools({
connectors: ["gmail"],
actions: ["email.send"],
});Utility web tools are advanced runtime capabilities:
profile.tools({
advanced: { utilitySearch: true },
});Raw file path access is advanced and lives under configure.files.*. Raw agent filesystem APIs are not part of the public default SDK shape.
Bulk historical/onboarding backfill is separate from runtime profile.commit():
const job = await configure.importProfiles({
mode: "backfill",
users: [
{
externalId: "customer-123",
profile: { preferences: ["Prefers concise replies."] },
conversations: [{
id: "thread-1",
messages: [{ role: "user", content: "I usually fly out of SFO." }],
}],
},
],
});
const status = await configure.importJobs.get(job.id);Import is server-side only, requires an sk_ key, and is not exposed as a model-facing tool.
Browser linking and components
Production browser integrations should use the hosted script:
<script src="https://configure.dev/js/configure.js"></script>Configure.link() handles user-present identity, seeding, consent, iframe isolation, resizing, and the configure:linked event. The host sends the returned agent-scoped token to its backend, then the backend uses configure.profile({ token }).
For chat inputs, Configure.personalizationButton() renders a compact + menu entry with the canonical Personalization toggle. The entry should trigger Configure, while linkEl mounts Configure Link in a dismissible inline chat panel. Let the panel span the assistant message lane (width: min(100%, var(--chat-max, 640px))) and do not cap the host at 420px or mount it inside the composer row. This is the default placement for chat products — not a settings page or a standalone chat-bar button. The onEvent callback exposes the agent-scoped token at event.payload.token; send only that token to your backend:
Configure.personalizationButton({
el: "#configure-entry",
linkEl: "#configure-link-host",
publishableKey: "pk_...",
agent: "your-agent",
displayName: "Your Agent",
font: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
onImage: () => openImagePicker(),
onFile: () => openFilePicker(),
onEvent(event) {
if (event.type === "configure:personalization-open") {
showInlineConfigurePanel();
return;
}
if (event.type === "configure:linked") {
hideInlineConfigurePanel();
fetch("/api/configure/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: event.payload.token }),
});
}
},
});displayName is cosmetic UI copy. The agent handle is the identity used for tokens, storage, permissions, and attribution.
Images and Files stay host-owned; the helper also emits configure:image-select and configure:file-select. (The declarative data-configure-link trigger instead exposes the token at event.detail.token — use it only for settings pages or static buttons.)
If your chat UI already has an inline integrations list instead of a + menu, render Configure as a rounded lockup button:
Configure.personalizationButton({
el: "#configure-integration",
publishableKey: "pk_...",
agent: "your-agent",
displayName: "Your Agent",
variant: "integration",
});Never expose sk_ keys in browser code. Browser code uses publishable keys (pk_...); the model receives Configure tool results or formatted approved profile context, not Link tokens or user IDs.
When Configure is the auth handoff for your app, use the hosted first-party surface instead of building a custom OTP flow:
Configure.signInWithPopup({
publishableKey: "pk_...",
agent: "your-agent",
displayName: "Your Agent",
returnTo: "https://app.example/auth/configure/callback",
state: "opaque",
fallback: "redirect",
});Allowlist returnTo with POST /v1/auth/sign-in/return-destinations using your sk_ key, then exchange the returned cfgsic_... code server-side with POST /v1/auth/sign-in/exchange. Store the returned agent-scoped token in your app session and pass it into later inline Configure surfaces with Configure.link({ token, userId, ... }) so the user does not repeat OTP. Use the popup path when preserving desktop app context matters; redirect to https://accounts.configure.dev/?pk=...&agent=...&return_to=... for mobile, popup-blocked browsers, or simpler auth routes.
Raw web components are included for local labs and advanced self-hosted surfaces:
import "configure/components";The browser bundle is also packaged at configure/components/cdn, and the narrow configure/browser entry point exists for browser bundlers. The hosted script remains the recommended production path.
