@keemakr/agent-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/client.d.ts +35 -0
- package/dist/client.js +78 -0
- package/dist/grant-auth.d.ts +19 -0
- package/dist/grant-auth.js +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @keemakr/agent-sdk
|
|
2
|
+
|
|
3
|
+
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:
|
|
4
|
+
|
|
5
|
+
- **verify** the operator's capability grant on every inbound delegation, and
|
|
6
|
+
- **reach tenant connections** (and, in later versions, memory and shared tools) through keemakr-core — **without holding raw secrets** and **without resolving the tenant yourself**.
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @keemakr/agent-sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer dependencies (match your eve agent): `eve@0.13.0`, `jose@^6.2.3`.
|
|
17
|
+
|
|
18
|
+
## Configure
|
|
19
|
+
|
|
20
|
+
Set these in your deployed agent's environment:
|
|
21
|
+
|
|
22
|
+
| Variable | Purpose |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `KEE_CORE_JWKS_URL` | keemakr-core's JWKS endpoint, e.g. `https://app.keemakr.com/.well-known/jwks.json`. Enables grant verification. |
|
|
25
|
+
| `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. |
|
|
26
|
+
| `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.) |
|
|
27
|
+
|
|
28
|
+
If `KEE_CORE_JWKS_URL` is unset, `grantAuth()` skips entirely — useful during local development.
|
|
29
|
+
|
|
30
|
+
## 1. Verify the grant in your channel
|
|
31
|
+
|
|
32
|
+
`grantAuth()` returns an eve `AuthFn`. Put it ahead of any fallback:
|
|
33
|
+
|
|
34
|
+
```ts title="agent/channels/eve.ts"
|
|
35
|
+
import { localDev, vercelOidc } from "eve/channels/auth";
|
|
36
|
+
import { eveChannel } from "eve/channels/eve";
|
|
37
|
+
import { grantAuth } from "@keemakr/agent-sdk";
|
|
38
|
+
|
|
39
|
+
export default eveChannel({
|
|
40
|
+
auth: [localDev(), vercelOidc(), grantAuth()],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
On success the verified tenant id and scopes are attached to the session auth context, where `useKee` reads them.
|
|
45
|
+
|
|
46
|
+
## 2. Reach tenant data from a tool
|
|
47
|
+
|
|
48
|
+
```ts title="agent/tools/find_email.ts"
|
|
49
|
+
import { defineTool } from "eve/tools";
|
|
50
|
+
import { z } from "zod";
|
|
51
|
+
import { useKee } from "@keemakr/agent-sdk";
|
|
52
|
+
|
|
53
|
+
export default defineTool({
|
|
54
|
+
description: "Find a lead's work email.",
|
|
55
|
+
inputSchema: z.object({
|
|
56
|
+
domain: z.string(),
|
|
57
|
+
first_name: z.string(),
|
|
58
|
+
last_name: z.string(),
|
|
59
|
+
}),
|
|
60
|
+
async execute(args, ctx) {
|
|
61
|
+
const kee = useKee(ctx);
|
|
62
|
+
// Proxy path: the credential stays in keemakr-core; you get the result.
|
|
63
|
+
const result = await kee.connections.hunter.call("email-finder", args);
|
|
64
|
+
return result; // { email, score, status }
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Connections API
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const kee = useKee(ctx);
|
|
73
|
+
kee.tenantId; // the tenant this delegation is for (from the grant)
|
|
74
|
+
kee.scopes; // the scopes the grant carries
|
|
75
|
+
|
|
76
|
+
// Proxy (default): run a named operation; the secret never leaves core.
|
|
77
|
+
await kee.connections.hunter.call("email-finder", { domain, first_name, last_name });
|
|
78
|
+
await kee.connections.get("hunter").call("email-finder", { ... }); // equivalent
|
|
79
|
+
|
|
80
|
+
// Token (opt-in): only if your entry.json declared `access: "token"` on the
|
|
81
|
+
// dependency. Returns a short-lived credential you may use directly.
|
|
82
|
+
const { access_token } = await kee.connections.hunter.token();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A call whose grant lacks the required scope returns a `KeeError` with `status: 403`; an expired/invalid grant returns `status: 401`.
|
|
86
|
+
|
|
87
|
+
## Security model
|
|
88
|
+
|
|
89
|
+
- **Tenant always comes from the verified grant**, resolved server-side. Never pass a tenant id from tool input.
|
|
90
|
+
- **On the proxy path, credentials never leave keemakr-core.** You send operation args; core runs the third-party request with the tenant's credential and returns only the result.
|
|
91
|
+
- **The token path is opt-in and scope-gated** (`conn:<provider>:token`), declared per dependency in your `entry.json` (`"access": "token"`).
|
|
92
|
+
- Every capability call re-verifies the grant and enforces scope on the server.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface KeeError extends Error {
|
|
2
|
+
status?: number;
|
|
3
|
+
}
|
|
4
|
+
export interface KeeContext {
|
|
5
|
+
session?: {
|
|
6
|
+
auth?: {
|
|
7
|
+
current?: {
|
|
8
|
+
attributes?: Record<string, string | readonly string[] | undefined> | null;
|
|
9
|
+
} | null;
|
|
10
|
+
} | null;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
/** Per-provider connection surface. */
|
|
14
|
+
export interface KeeConnection {
|
|
15
|
+
/** Run a named proxy operation. The credential stays in core. */
|
|
16
|
+
call(op: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
17
|
+
/** Request a short-lived scoped token (opt-in; requires the conn:<p>:token scope). */
|
|
18
|
+
token(): Promise<{
|
|
19
|
+
access_token: string;
|
|
20
|
+
account_label: string | null;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface Kee {
|
|
24
|
+
tenantId: string;
|
|
25
|
+
scopes: string[];
|
|
26
|
+
connections: Record<string, KeeConnection> & {
|
|
27
|
+
/** Explicit accessor (equivalent to kee.connections[provider]). */
|
|
28
|
+
get(provider: string): KeeConnection;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a tenant-scoped capability client from a tool's context. Call inside a
|
|
33
|
+
* tool's execute: `const kee = useKee(ctx)`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useKee(ctx: KeeContext): Kee;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// useKee(ctx) — the tenant-scoped capability client a marketplace agent's tools
|
|
2
|
+
// use to reach tenant data through keemakr-core. It reads the verified grant off
|
|
3
|
+
// the session auth context (put there by grantAuth) and calls back into core's
|
|
4
|
+
// /api/capability/* endpoints, forwarding the grant. Core re-verifies the grant
|
|
5
|
+
// and enforces scope on every call; the SDK never sees a raw credential on the
|
|
6
|
+
// proxy path.
|
|
7
|
+
function keeError(message, status) {
|
|
8
|
+
const e = new Error(message);
|
|
9
|
+
e.name = 'KeeError';
|
|
10
|
+
e.status = status;
|
|
11
|
+
return e;
|
|
12
|
+
}
|
|
13
|
+
function readGrant(ctx) {
|
|
14
|
+
const attrs = ctx?.session?.auth?.current?.attributes ?? {};
|
|
15
|
+
const token = typeof attrs.grant_token === 'string' ? attrs.grant_token : undefined;
|
|
16
|
+
const tenantId = typeof attrs.tenant_id === 'string' ? attrs.tenant_id : undefined;
|
|
17
|
+
const scopes = Array.isArray(attrs.scopes) ? [...attrs.scopes] : [];
|
|
18
|
+
const traceId = typeof attrs.trace_id === 'string' ? attrs.trace_id : undefined;
|
|
19
|
+
if (!token || !tenantId) {
|
|
20
|
+
throw keeError('no capability grant on the session — was the request authenticated with grantAuth()?', 401);
|
|
21
|
+
}
|
|
22
|
+
return { token, tenantId, scopes, traceId };
|
|
23
|
+
}
|
|
24
|
+
// Resolve core's base URL. Prefer KEE_CORE_URL; otherwise derive it from the
|
|
25
|
+
// JWKS URL by stripping the well-known path.
|
|
26
|
+
function coreBaseUrl() {
|
|
27
|
+
const explicit = process.env.KEE_CORE_URL;
|
|
28
|
+
if (explicit)
|
|
29
|
+
return explicit.replace(/\/$/, '');
|
|
30
|
+
const jwks = process.env.KEE_CORE_JWKS_URL;
|
|
31
|
+
if (jwks)
|
|
32
|
+
return jwks.replace(/\/\.well-known\/jwks\.json\/?$/, '');
|
|
33
|
+
throw keeError('KEE_CORE_URL (or KEE_CORE_JWKS_URL) must be set to reach the Capability API');
|
|
34
|
+
}
|
|
35
|
+
async function capabilityFetch(grant, path, body) {
|
|
36
|
+
const url = `${coreBaseUrl()}/api/capability/${path}`;
|
|
37
|
+
const res = await fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'content-type': 'application/json',
|
|
41
|
+
authorization: `Bearer ${grant.token}`,
|
|
42
|
+
...(grant.traceId ? { 'x-keemakr-trace-id': grant.traceId } : {}),
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify(body ?? {}),
|
|
45
|
+
});
|
|
46
|
+
const json = (await res.json().catch(() => ({})));
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw keeError(json.error ?? `capability request failed (${res.status})`, res.status);
|
|
49
|
+
}
|
|
50
|
+
return json;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build a tenant-scoped capability client from a tool's context. Call inside a
|
|
54
|
+
* tool's execute: `const kee = useKee(ctx)`.
|
|
55
|
+
*/
|
|
56
|
+
export function useKee(ctx) {
|
|
57
|
+
const grant = readGrant(ctx);
|
|
58
|
+
const connectionFor = (provider) => ({
|
|
59
|
+
async call(op, args) {
|
|
60
|
+
const json = (await capabilityFetch(grant, `conn/${provider}/${op}`, {
|
|
61
|
+
args: args ?? {},
|
|
62
|
+
}));
|
|
63
|
+
return json.result;
|
|
64
|
+
},
|
|
65
|
+
async token() {
|
|
66
|
+
const json = (await capabilityFetch(grant, `conn/${provider}/token`, {}));
|
|
67
|
+
return { access_token: json.access_token, account_label: json.account_label };
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const connections = new Proxy({ get: connectionFor }, {
|
|
71
|
+
get(target, prop) {
|
|
72
|
+
if (prop === 'get')
|
|
73
|
+
return connectionFor;
|
|
74
|
+
return connectionFor(prop);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
return { tenantId: grant.tenantId, scopes: grant.scopes, connections };
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AuthFn } from 'eve/channels/auth';
|
|
2
|
+
/**
|
|
3
|
+
* An eve AuthFn that accepts a keemakr capability grant. Returns a principal
|
|
4
|
+
* carrying `tenant_id`, `scopes`, and the raw `grant_token` in attributes on
|
|
5
|
+
* success, or `null` to skip to the next auth entry (so it composes before any
|
|
6
|
+
* fallback).
|
|
7
|
+
*
|
|
8
|
+
* Environment:
|
|
9
|
+
* KEE_CORE_JWKS_URL keemakr-core's JWKS endpoint
|
|
10
|
+
* (e.g. https://app.keemakr.com/.well-known/jwks.json).
|
|
11
|
+
* If unset, this AuthFn skips entirely (grant path off).
|
|
12
|
+
* KEE_AGENT_AUDIENCE this deployment's audience — the runtime URL's origin —
|
|
13
|
+
* matching the `aud` the operator mints. If unset, the
|
|
14
|
+
* audience check is skipped (dev convenience only).
|
|
15
|
+
*/
|
|
16
|
+
export declare function grantAuth(opts?: {
|
|
17
|
+
jwksUrl?: string;
|
|
18
|
+
audience?: string;
|
|
19
|
+
}): AuthFn<Request>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Capability-grant verification for a remote keemakr agent.
|
|
2
|
+
//
|
|
3
|
+
// The keemakr operator attaches a short-lived signed JWT — a "capability grant" —
|
|
4
|
+
// on every delegation. It carries a VERIFIABLE tenant id + scopes (+ a trace id),
|
|
5
|
+
// signed with keemakr-core's RS256 key and verifiable against the JWKS it
|
|
6
|
+
// publishes at /.well-known/jwks.json.
|
|
7
|
+
//
|
|
8
|
+
// grantAuth() returns an eve AuthFn that verifies the grant against core's JWKS
|
|
9
|
+
// and surfaces the tenant + scopes (and the raw grant, for useKee to forward) on
|
|
10
|
+
// the session auth context. Use it as the PRIMARY inbound auth in your channel.
|
|
11
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
12
|
+
import { extractBearerToken } from 'eve/channels/auth';
|
|
13
|
+
// The issuer keemakr-core mints grants with.
|
|
14
|
+
const GRANT_ISSUER = 'keemakr';
|
|
15
|
+
let jwks = null;
|
|
16
|
+
function jwksFor(url) {
|
|
17
|
+
if (!jwks)
|
|
18
|
+
jwks = createRemoteJWKSet(new URL(url));
|
|
19
|
+
return jwks;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An eve AuthFn that accepts a keemakr capability grant. Returns a principal
|
|
23
|
+
* carrying `tenant_id`, `scopes`, and the raw `grant_token` in attributes on
|
|
24
|
+
* success, or `null` to skip to the next auth entry (so it composes before any
|
|
25
|
+
* fallback).
|
|
26
|
+
*
|
|
27
|
+
* Environment:
|
|
28
|
+
* KEE_CORE_JWKS_URL keemakr-core's JWKS endpoint
|
|
29
|
+
* (e.g. https://app.keemakr.com/.well-known/jwks.json).
|
|
30
|
+
* If unset, this AuthFn skips entirely (grant path off).
|
|
31
|
+
* KEE_AGENT_AUDIENCE this deployment's audience — the runtime URL's origin —
|
|
32
|
+
* matching the `aud` the operator mints. If unset, the
|
|
33
|
+
* audience check is skipped (dev convenience only).
|
|
34
|
+
*/
|
|
35
|
+
export function grantAuth(opts) {
|
|
36
|
+
return async (request) => {
|
|
37
|
+
const jwksUrl = opts?.jwksUrl ?? process.env.KEE_CORE_JWKS_URL;
|
|
38
|
+
if (!jwksUrl)
|
|
39
|
+
return null;
|
|
40
|
+
const token = extractBearerToken(request.headers.get('authorization'));
|
|
41
|
+
if (!token)
|
|
42
|
+
return null;
|
|
43
|
+
const expectedAud = opts?.audience ?? process.env.KEE_AGENT_AUDIENCE;
|
|
44
|
+
try {
|
|
45
|
+
const { payload } = await jwtVerify(token, jwksFor(jwksUrl), {
|
|
46
|
+
issuer: GRANT_ISSUER,
|
|
47
|
+
...(expectedAud ? { audience: expectedAud } : {}),
|
|
48
|
+
});
|
|
49
|
+
const tenantId = payload.tenant_id;
|
|
50
|
+
const scopes = payload.scopes;
|
|
51
|
+
const installedAgent = payload.installed_agent;
|
|
52
|
+
const traceId = payload.trace_id;
|
|
53
|
+
if (typeof tenantId !== 'string' || !Array.isArray(scopes))
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
authenticator: 'keemakr-grant',
|
|
57
|
+
issuer: GRANT_ISSUER,
|
|
58
|
+
principalId: typeof installedAgent === 'string' ? installedAgent : 'keemakr-agent',
|
|
59
|
+
principalType: 'service',
|
|
60
|
+
subject: typeof payload.sub === 'string' ? payload.sub : undefined,
|
|
61
|
+
attributes: {
|
|
62
|
+
via: 'grant',
|
|
63
|
+
tenant_id: tenantId,
|
|
64
|
+
scopes: scopes.map(String),
|
|
65
|
+
grant_token: token,
|
|
66
|
+
...(typeof traceId === 'string' ? { trace_id: traceId } : {}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @keemakr/agent-sdk — the floor for keemakr marketplace agents.
|
|
2
|
+
//
|
|
3
|
+
// Inbound: verify the operator's capability grant in your channel.
|
|
4
|
+
// import { grantAuth } from '@keemakr/agent-sdk';
|
|
5
|
+
// export default eveChannel({ auth: [localDev(), grantAuth()] });
|
|
6
|
+
//
|
|
7
|
+
// Inside a tool: reach tenant data through keemakr-core, without holding secrets.
|
|
8
|
+
// import { useKee } from '@keemakr/agent-sdk';
|
|
9
|
+
// const kee = useKee(ctx);
|
|
10
|
+
// const r = await kee.connections.hunter.call('email-finder', { domain, first_name, last_name });
|
|
11
|
+
export { grantAuth } from './grant-auth.js';
|
|
12
|
+
export { useKee } from './client.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keemakr/agent-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The floor for keemakr marketplace agents: verify the capability grant and reach tenant connections (and, soon, memory and shared tools) through keemakr-core — without holding raw secrets or resolving the tenant yourself.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/fsztpartners/keemakr-agent-sdk.git"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"eve": "0.13.0",
|
|
33
|
+
"jose": "^6.2.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.19.43",
|
|
37
|
+
"eve": "0.13.0",
|
|
38
|
+
"jose": "^6.2.3",
|
|
39
|
+
"typescript": "^5"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.9"
|
|
43
|
+
}
|
|
44
|
+
}
|