@keemakr/agent-sdk
v0.9.0
Published
The floor for keemakr marketplace agents: verify the capability grant and reach tenant connections, memory, and shared platform tools through keemakr-core — without holding raw secrets or resolving the tenant yourself.
Downloads
1,332
Readme
@keemakr/agent-sdk
The floor for keemakr marketplace agents — separately deployed eve agents that the keemakr operator delegates to. The SDK gives your agent a stable, secure contract to:
- verify the operator's capability grant on every inbound delegation, and
- reach tenant connections (and, in later versions, memory and shared tools) through keemakr-core — without holding raw secrets and without resolving the tenant yourself.
The tenant identity and scopes come from a short-lived signed grant the operator mints per delegation; keemakr-core re-verifies the grant and enforces scope on every capability call. Connection credentials never leave keemakr-core on the default (proxy) path.
Install
npm install @keemakr/agent-sdkPeer dependencies (match your eve agent): [email protected], jose@^6.2.3.
Configure
Set these in your deployed agent's environment:
| Variable | Purpose |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| KEE_CORE_JWKS_URL | keemakr-core's JWKS endpoint, e.g. https://app.keemakr.com/.well-known/jwks.json. Enables grant verification. |
| KEE_AGENT_AUDIENCE | This deployment's audience — your runtime URL's origin, e.g. https://my-agent.example.com. Must match the audience the operator mints. |
| KEE_CORE_URL | keemakr-core's base URL for capability calls, e.g. https://app.keemakr.com. (Derived from KEE_CORE_JWKS_URL if unset.) |
If KEE_CORE_JWKS_URL is unset, grantAuth() skips entirely — useful during local development.
1. Verify the grant in your channel
grantAuth() returns an eve AuthFn. Put it ahead of any fallback:
import { localDev, vercelOidc } from 'eve/channels/auth';
import { eveChannel } from 'eve/channels/eve';
import { grantAuth } from '@keemakr/agent-sdk';
export default eveChannel({
auth: [localDev(), vercelOidc(), grantAuth()],
});On success the verified tenant id and scopes are attached to the session auth context, where useKee reads them.
2. Reach tenant data from a tool
import { defineTool } from 'eve/tools';
import { z } from 'zod';
import { useKee } from '@keemakr/agent-sdk';
export default defineTool({
description: "Find a lead's work email.",
inputSchema: z.object({
domain: z.string(),
first_name: z.string(),
last_name: z.string(),
}),
async execute(args, ctx) {
const kee = useKee(ctx);
// Proxy path: the credential stays in keemakr-core; you get the result.
const result = await kee.connections.hunter.call('email-finder', args);
return result; // { email, score, status }
},
});Connections API
const kee = useKee(ctx);
kee.tenantId; // the tenant this delegation is for (from the grant)
kee.scopes; // the scopes the grant carries
// Proxy (default): run a named operation; the secret never leaves core.
await kee.connections.hunter.call("email-finder", { domain, first_name, last_name });
await kee.connections.get("hunter").call("email-finder", { ... }); // equivalent
// Token (opt-in): only if your entry.json declared `access: "token"` on the
// dependency. Returns a short-lived credential you may use directly.
const { access_token } = await kee.connections.hunter.token();Discover connectors + operations
@keemakr/agent-sdk/connectors ships a generated, typed manifest of every connector keemakr-core exposes — provider slugs, maturity, and each operation's name + JSON-Schema arg contract. Use it to discover what's callable (and get autocomplete on provider + op names) without scanning a core checkout or hitting a running instance. It's metadata only — no credentials.
import { connectors, opNames, isReady } from '@keemakr/agent-sdk/connectors';
opNames('hunter'); // → ["email-finder"]
connectors.hunter.ops['email-finder'].inputSchema; // JSON Schema for the args
isReady('meta'); // false while a connector is coming_soon
connectors.meta.maturity; // "coming_soon" | "ready"A coming_soon connector is declarable in your entry.json dependencies today; its operations start callable (and isReady flips to true) once core ships them — no change to your agent.
Refresh the manifest after core ships new connectors/operations (it's a committed snapshot of GET /api/connections/catalog):
curl -s "$KEE_CORE_URL/api/connections/catalog" > src/connectors.snapshot.json
npm run gen:connectors # or: npm run build (runs gen first)Memory (cross-session, tenant-shared)
await kee.memory.set('prefs', 'tone', { tone: 'formal' });
await kee.memory.get('prefs', 'tone'); // → { tone: "formal" }
await kee.memory.list('prefs'); // → entries in the namespace
await kee.memory.delete('prefs', 'tone');
// Semantic search by meaning (embeddings):
const hits = await kee.memory.search('how should I speak to the user?', { limit: 5 });
// → [{ namespace, key, value, score, … }] (score 0–1, nearest first)Memory is tenant-shared: any of the tenant's installed agents can read/write any
namespace. Concurrent writers should take turns — every entry carries a
monotonic version, and conditional writes lose gracefully instead of
clobbering:
const entry = await kee.memory.getEntry('crm', 'lead:acme'); // { value, version, … }
try {
await kee.memory.set('crm', 'lead:acme', next, { ifVersion: entry!.version });
} catch (e) {
if (e instanceof MemoryConflictError) {
// someone wrote first — e.current is the winning entry; re-read, re-derive, retry
}
}
// Or merge one field with no read at all (object values only):
await kee.memory.patch('crm', 'lead:acme', { status: 'contacted' });Use memory for your agent's own continuity (preferences, cursors, entity
state) — durable documents belong in the tenant knowledge base, which you read
via kee.kb.
Knowledge base (read-only retrieval)
const hits = await kee.kb.search('what is our refund policy?', { k: 5 });
// → [{ text, score, provenance: { title, source_uri, … } }]Scoped server-side to the collections bound to your agent + the tenant's
default corpus + the shared platform KB (kb:retrieve scope, granted to every
install). Hybrid retrieval, reranked in core; text may be a wider parent
context for clause-level documents.
Platform tools
await kee.tools.list(); // tools this grant is entitled to
await kee.tools.run('current-time'); // run one in keemakr-coreA call whose grant lacks the required scope returns a KeeError with status: 403; an expired/invalid grant returns status: 401.
Grant refresh (automatic since 0.8.0)
Every useKee capability call keeps its grant alive by itself: when the active token has under two minutes left, the SDK exchanges it at core's POST /api/capability/grant/refresh (single-flight per delegation — concurrent tool calls share one refresh), and a 401 grant_expired gets one refresh + one retry before surfacing. You write nothing; long runs simply stop dying at the TTL.
The floor is core's, not the SDK's: an expired grant can never be refreshed, scopes are re-derived from the install at each exchange, and the whole chain dies at the renewal horizon (CAPABILITY_GRANT_MAX_LIFETIME_SECONDS on core, default 6h) with a relayable grant_horizon_exceeded error.
Headless callers holding a raw grant can drive the exchange directly:
import { refreshGrant } from '@keemakr/agent-sdk';
const { token, exp } = await refreshGrant(currentToken); // throws KeeError when core refusesrefreshGrant resolves core from KEE_CORE_URL / KEE_CORE_JWKS_URL (pass { coreUrl } to override) and never verifies or signs anything locally — core is the only judge.
Autonomous / scheduled runs
A cron/scheduled turn has no operator session, so it gets no session grant. keemakr-core can mint a machine grant for it (gated on the tenant's per-install unattended_consent). If your remote runs outside an eve channel, verify that grant directly:
import { verifyGrant } from '@keemakr/agent-sdk';
const claims = await verifyGrant(grantToken, { audience: process.env.KEE_AGENT_AUDIENCE });
if (!claims) throw new Error('invalid or expired grant');
// claims.tenantId, claims.scopes, claims.aud, claims.expInside an eve channel, grantAuth() already accepts machine grants (same token shape) — no extra work.
Credential & model contract
- Tenant/service credentials live in keemakr-core, reached only via the proxy — the credential never crosses the wire. Tenant is always the verified grant, resolved server-side; never pass a tenant id from tool input.
- The token path is opt-in and scope-gated (
conn:<provider>:token), declared per dependency inentry.json("access": "token"). - You MAY hold your own model key. There is no platform model gateway today, so an agent routing its own LLM calls (its own Anthropic/AI-Gateway key) is expected and fine — that is not a credential leak. A leak is a tenant/service credential read in agent code.
- Every capability call re-verifies the grant and enforces scope on the server.
Full contract: keemakr-core docs/CONNECTOR-CONTRACT.md.
License
MIT
