@keemakr/agent-sdk 0.10.0 → 0.11.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 CHANGED
@@ -19,13 +19,21 @@ Peer dependencies (match your eve agent): `eve@0.13.0`, `jose@^6.2.3`.
19
19
 
20
20
  Set these in your deployed agent's environment:
21
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.
22
+ | Variable | Purpose |
23
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `KEE_CORE_URL` | **Required.** The keemakr platform you installed on, e.g. `https://dash.dev.keemakr.ai`. Used both to verify grants and to call the capability API. |
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_JWKS_URL` | Only when the JWKS is not at `<KEE_CORE_URL>/.well-known/jwks.json`. Takes precedence over `KEE_CORE_URL` for verification. |
27
+
28
+ **There is no default platform, deliberately.** `KEE_CORE_URL` names the keys your
29
+ agent trusts and the host it sends the tenant's grant token to. A compiled-in
30
+ default would mean this package picking your trust anchor for you — and since the
31
+ same published SDK is installed against dev, staging and production, whichever
32
+ origin it named would be wrong for the others. Set one variable per deployment.
33
+
34
+ With neither set, `grantAuth()` refuses every grant and logs once explaining why,
35
+ and `coreBaseUrl()` throws rather than posting a live credential somewhere you did
36
+ not choose. Both name the variables in their message.
29
37
 
30
38
  ## 1. Verify the grant in your channel
31
39
 
@@ -142,13 +150,17 @@ via `kee.kb`.
142
150
 
143
151
  ```ts
144
152
  const hits = await kee.kb.search('what is our refund policy?', { k: 5 });
145
- // → [{ text, score, provenance: { title, source_uri, … } }]
153
+ // → [{ text, score, artifact_id, version_id, chunk_id, title,
154
+ // audience, visibility, provenance }]
146
155
  ```
147
156
 
148
- Scoped server-side to the collections bound to your agent + the tenant's
149
- default corpus + the shared platform KB (`kb:retrieve` scope, granted to every
150
- install). Hybrid retrieval, reranked in core; `text` may be a wider parent
151
- context for clause-level documents.
157
+ Core derives the retrieval actor from the verified grant. A Kee acting for a
158
+ user can retrieve that user's Personal Knowledge plus Organization and Platform
159
+ Knowledge. A System Kee can retrieve Organization and Platform Knowledge but
160
+ never Personal Knowledge. External Kees are additionally restricted to External
161
+ Safe knowledge. Collections organize artifacts but never grant access
162
+ (`kb:retrieve` scope, granted to every install). Retrieval is hybrid and may be
163
+ reranked in Core; `text` may be a wider parent context.
152
164
 
153
165
  ### Platform tools
154
166
 
package/dist/client.d.ts CHANGED
@@ -112,13 +112,17 @@ export interface KeeMemory {
112
112
  export interface KBHit {
113
113
  text: string;
114
114
  score: number;
115
+ artifact_id: string;
116
+ version_id: string;
117
+ chunk_id: string;
118
+ title: string;
119
+ audience: 'personal' | 'organization' | 'platform';
120
+ visibility: 'internal_only' | 'external_safe';
115
121
  provenance: Record<string, unknown>;
116
122
  }
117
123
  /**
118
- * Tenant knowledge-base retrieval. The agent sees the collections bound to it
119
- * plus the tenant's default corpus plus the shared platform KB — scoping is
120
- * enforced server-side from the grant. Requires the `kb:retrieve` scope
121
- * (granted to every install).
124
+ * Governed Company Brain retrieval. Core derives User, System, or External Kee access from the
125
+ * verified grant and returns source identities suitable for citations. Requires `kb:retrieve`.
122
126
  */
123
127
  export interface KeeKb {
124
128
  /** Semantic + lexical + reranked search over the agent-visible knowledge. */
@@ -182,6 +186,31 @@ export interface KeeTools {
182
186
  /** Run a registry tool by name and return its result. Requires `tools:run`. */
183
187
  run(name: string, args?: Record<string, unknown>): Promise<unknown>;
184
188
  }
189
+ /**
190
+ * The entry's STAFF-SET platform settings — one value per `kind:'env'`
191
+ * dependency it declares. Requires the `config:read` scope, which that same
192
+ * declaration mints.
193
+ *
194
+ * These are operator settings (a destination address, a shared endpoint), not
195
+ * tenant data and not secrets the agent may change: the surface is read-only by
196
+ * design. An operator sets them once in the platform console and they apply to
197
+ * every install of the entry.
198
+ */
199
+ export interface KeeConfig {
200
+ /** One setting, or null when staff have not set it yet. */
201
+ get(key: string): Promise<string | null>;
202
+ /**
203
+ * Every declared setting at once — `{ config, declared }`. `declared` lists
204
+ * the keys this entry asks for, so "staff haven't set it" (declared, missing
205
+ * from config) stays distinguishable from "this entry has no such setting"
206
+ * (absent from declared). Collapsing those two is what makes a configuration
207
+ * gap look like a runtime bug.
208
+ */
209
+ all(): Promise<{
210
+ config: Record<string, string>;
211
+ declared: string[];
212
+ }>;
213
+ }
185
214
  export interface Kee {
186
215
  tenantId: string;
187
216
  scopes: string[];
@@ -193,6 +222,7 @@ export interface Kee {
193
222
  records: KeeRecords;
194
223
  kb: KeeKb;
195
224
  tools: KeeTools;
225
+ config: KeeConfig;
196
226
  }
197
227
  /**
198
228
  * Build a tenant-scoped capability client from a tool's context. Call inside a
package/dist/client.js CHANGED
@@ -278,7 +278,7 @@ export function useKee(ctx) {
278
278
  async search(query, opts) {
279
279
  const json = (await capabilityFetch(grant, 'kb/retrieve', {
280
280
  query,
281
- k: opts?.k,
281
+ limit: opts?.k,
282
282
  }));
283
283
  return json.hits ?? [];
284
284
  },
@@ -295,6 +295,30 @@ export function useKee(ctx) {
295
295
  return json.result;
296
296
  },
297
297
  };
298
+ // One fetch serves both accessors, and the result is cached for the life of
299
+ // this client: settings change at operator pace, not per tool call, and a
300
+ // `get()` per setting inside one tool would re-fetch the same map N times.
301
+ let configOnce = null;
302
+ const config = {
303
+ async all() {
304
+ configOnce ??= (async () => {
305
+ const json = (await capabilityFetch(grant, 'config', undefined, 'GET'));
306
+ return { config: json.config ?? {}, declared: json.declared ?? [] };
307
+ })();
308
+ try {
309
+ return await configOnce;
310
+ }
311
+ catch (e) {
312
+ // Don't cache a failure — a transient error must not poison every later
313
+ // read for the rest of the session.
314
+ configOnce = null;
315
+ throw e;
316
+ }
317
+ },
318
+ async get(key) {
319
+ return (await this.all()).config[key] ?? null;
320
+ },
321
+ };
298
322
  return {
299
323
  tenantId: grant.tenantId,
300
324
  scopes: grant.scopes,
@@ -303,5 +327,6 @@ export function useKee(ctx) {
303
327
  records,
304
328
  kb,
305
329
  tools,
330
+ config,
306
331
  };
307
332
  }
@@ -1,11 +1,10 @@
1
- export type ConnectorName = "agent-sessions" | "apify" | "apollo" | "cal-com" | "calendly" | "clearbit" | "drip" | "elevenlabs" | "facebook" | "fal" | "hubspot" | "hunter" | "instagram" | "instantly" | "mailchimp" | "meta" | "pagespeed" | "posthog" | "salesforce" | "sendgrid" | "slack" | "stripe" | "twilio" | "whatsapp" | "wordpress";
1
+ export type ConnectorName = "agent-sessions" | "apify" | "apollo" | "cal-com" | "clearbit" | "drip" | "elevenlabs" | "facebook" | "fal" | "hubspot" | "hunter" | "instagram" | "instantly" | "mailchimp" | "meta" | "pagespeed" | "posthog" | "sendgrid" | "stripe" | "twilio" | "wordpress";
2
2
  /** Per-provider operation names (autocomplete for `.call(op)`). */
3
3
  export interface ConnectorOps {
4
4
  "agent-sessions": never;
5
5
  "apify": never;
6
6
  "apollo": never;
7
7
  "cal-com": never;
8
- "calendly": never;
9
8
  "clearbit": never;
10
9
  "drip": never;
11
10
  "elevenlabs": never;
@@ -19,12 +18,9 @@ export interface ConnectorOps {
19
18
  "meta": never;
20
19
  "pagespeed": never;
21
20
  "posthog": never;
22
- "salesforce": never;
23
21
  "sendgrid": never;
24
- "slack": never;
25
22
  "stripe": never;
26
23
  "twilio": never;
27
- "whatsapp": never;
28
24
  "wordpress": never;
29
25
  }
30
26
  export interface ConnectorOp {
@@ -79,14 +75,6 @@ export declare const connectors: {
79
75
  readonly docUrl: "https://cal.com/docs/api-reference";
80
76
  readonly ops: {};
81
77
  };
82
- readonly calendly: {
83
- readonly displayName: "Calendly";
84
- readonly authKind: "api_key";
85
- readonly category: "scheduling";
86
- readonly maturity: "coming_soon";
87
- readonly docUrl: "https://developer.calendly.com/api-docs";
88
- readonly ops: {};
89
- };
90
78
  readonly clearbit: {
91
79
  readonly displayName: "Clearbit";
92
80
  readonly authKind: "api_key";
@@ -217,14 +205,6 @@ export declare const connectors: {
217
205
  readonly docUrl: "https://posthog.com/docs/api";
218
206
  readonly ops: {};
219
207
  };
220
- readonly salesforce: {
221
- readonly displayName: "Salesforce";
222
- readonly authKind: "api_key";
223
- readonly category: "crm";
224
- readonly maturity: "coming_soon";
225
- readonly docUrl: "https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/";
226
- readonly ops: {};
227
- };
228
208
  readonly sendgrid: {
229
209
  readonly displayName: "SendGrid";
230
210
  readonly authKind: "api_key";
@@ -233,14 +213,6 @@ export declare const connectors: {
233
213
  readonly docUrl: "https://docs.sendgrid.com/api-reference";
234
214
  readonly ops: {};
235
215
  };
236
- readonly slack: {
237
- readonly displayName: "Slack";
238
- readonly authKind: "api_key";
239
- readonly category: "messaging";
240
- readonly maturity: "coming_soon";
241
- readonly docUrl: "https://api.slack.com/methods/chat.postMessage";
242
- readonly ops: {};
243
- };
244
216
  readonly stripe: {
245
217
  readonly displayName: "Stripe";
246
218
  readonly authKind: "oauth2";
@@ -257,14 +229,6 @@ export declare const connectors: {
257
229
  readonly docUrl: "https://www.twilio.com/docs/usage/api";
258
230
  readonly ops: {};
259
231
  };
260
- readonly whatsapp: {
261
- readonly displayName: "WhatsApp";
262
- readonly authKind: "oauth2";
263
- readonly category: "channel";
264
- readonly maturity: "coming_soon";
265
- readonly docUrl: "https://developers.facebook.com/docs/whatsapp";
266
- readonly ops: {};
267
- };
268
232
  readonly wordpress: {
269
233
  readonly displayName: "WordPress";
270
234
  readonly authKind: "oauth2";
@@ -39,14 +39,6 @@ export const connectors = {
39
39
  "docUrl": "https://cal.com/docs/api-reference",
40
40
  "ops": {}
41
41
  },
42
- "calendly": {
43
- "displayName": "Calendly",
44
- "authKind": "api_key",
45
- "category": "scheduling",
46
- "maturity": "coming_soon",
47
- "docUrl": "https://developer.calendly.com/api-docs",
48
- "ops": {}
49
- },
50
42
  "clearbit": {
51
43
  "displayName": "Clearbit",
52
44
  "authKind": "api_key",
@@ -181,14 +173,6 @@ export const connectors = {
181
173
  "docUrl": "https://posthog.com/docs/api",
182
174
  "ops": {}
183
175
  },
184
- "salesforce": {
185
- "displayName": "Salesforce",
186
- "authKind": "api_key",
187
- "category": "crm",
188
- "maturity": "coming_soon",
189
- "docUrl": "https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/",
190
- "ops": {}
191
- },
192
176
  "sendgrid": {
193
177
  "displayName": "SendGrid",
194
178
  "authKind": "api_key",
@@ -197,14 +181,6 @@ export const connectors = {
197
181
  "docUrl": "https://docs.sendgrid.com/api-reference",
198
182
  "ops": {}
199
183
  },
200
- "slack": {
201
- "displayName": "Slack",
202
- "authKind": "api_key",
203
- "category": "messaging",
204
- "maturity": "coming_soon",
205
- "docUrl": "https://api.slack.com/methods/chat.postMessage",
206
- "ops": {}
207
- },
208
184
  "stripe": {
209
185
  "displayName": "Stripe",
210
186
  "authKind": "oauth2",
@@ -221,14 +197,6 @@ export const connectors = {
221
197
  "docUrl": "https://www.twilio.com/docs/usage/api",
222
198
  "ops": {}
223
199
  },
224
- "whatsapp": {
225
- "displayName": "WhatsApp",
226
- "authKind": "oauth2",
227
- "category": "channel",
228
- "maturity": "coming_soon",
229
- "docUrl": "https://developers.facebook.com/docs/whatsapp",
230
- "ops": {}
231
- },
232
200
  "wordpress": {
233
201
  "displayName": "WordPress",
234
202
  "authKind": "oauth2",
@@ -6,12 +6,22 @@ import { type AuthFn } from 'eve/channels/auth';
6
6
  * fallback).
7
7
  *
8
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).
9
+ * KEE_CORE_URL core's origin, e.g. https://dash.<env>.keemakr.ai — the
10
+ * platform this agent was installed on. REQUIRED (unless
11
+ * KEE_CORE_JWKS_URL or `opts.jwksUrl` says the same thing):
12
+ * it names the keys this agent trusts, and there is no
13
+ * default, because a default would mean trusting whichever
14
+ * platform the SDK author picked. Without it, every grant is
15
+ * refused and the reason is logged once.
16
+ * KEE_CORE_JWKS_URL the JWKS endpoint directly, when it is not
17
+ * `<KEE_CORE_URL>/.well-known/jwks.json`.
12
18
  * KEE_AGENT_AUDIENCE this deployment's audience — the runtime URL's origin —
13
19
  * matching the `aud` the operator mints. If unset, the
14
20
  * audience check is skipped (dev convenience only).
21
+ *
22
+ * A missing configuration no longer switches grant verification off SILENTLY,
23
+ * which was the real defect: it used to return the same bare null as a bad token,
24
+ * so every capability call 401'd with nothing in the log to say why.
15
25
  */
16
26
  export declare function grantAuth(opts?: {
17
27
  jwksUrl?: string;
@@ -8,16 +8,11 @@
8
8
  // grantAuth() returns an eve AuthFn that verifies the grant against core's JWKS
9
9
  // and surfaces the tenant + scopes (and the raw grant, for useKee to forward) on
10
10
  // the session auth context. Use it as the PRIMARY inbound auth in your channel.
11
- import { createRemoteJWKSet, jwtVerify } from 'jose';
11
+ import { jwtVerify } from 'jose';
12
12
  import { extractBearerToken } from 'eve/channels/auth';
13
+ import { jwksFor, resolveJwksUrl, warnCoreUnconfigured, warnGrantVerifyFailed } from './jwks.js';
13
14
  // The issuer keemakr-core mints grants with.
14
15
  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
16
  /**
22
17
  * An eve AuthFn that accepts a keemakr capability grant. Returns a principal
23
18
  * carrying `tenant_id`, `scopes`, and the raw `grant_token` in attributes on
@@ -25,21 +20,35 @@ function jwksFor(url) {
25
20
  * fallback).
26
21
  *
27
22
  * 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).
23
+ * KEE_CORE_URL core's origin, e.g. https://dash.<env>.keemakr.ai — the
24
+ * platform this agent was installed on. REQUIRED (unless
25
+ * KEE_CORE_JWKS_URL or `opts.jwksUrl` says the same thing):
26
+ * it names the keys this agent trusts, and there is no
27
+ * default, because a default would mean trusting whichever
28
+ * platform the SDK author picked. Without it, every grant is
29
+ * refused and the reason is logged once.
30
+ * KEE_CORE_JWKS_URL the JWKS endpoint directly, when it is not
31
+ * `<KEE_CORE_URL>/.well-known/jwks.json`.
31
32
  * KEE_AGENT_AUDIENCE this deployment's audience — the runtime URL's origin —
32
33
  * matching the `aud` the operator mints. If unset, the
33
34
  * audience check is skipped (dev convenience only).
35
+ *
36
+ * A missing configuration no longer switches grant verification off SILENTLY,
37
+ * which was the real defect: it used to return the same bare null as a bad token,
38
+ * so every capability call 401'd with nothing in the log to say why.
34
39
  */
35
40
  export function grantAuth(opts) {
36
41
  return async (request) => {
37
- const jwksUrl = opts?.jwksUrl ?? process.env.KEE_CORE_JWKS_URL;
38
- if (!jwksUrl)
39
- return null;
42
+ // Token first: an unconfigured deployment should complain about requests that
43
+ // actually carry a grant, not about every anonymous hit on the channel.
40
44
  const token = extractBearerToken(request.headers.get('authorization'));
41
45
  if (!token)
42
46
  return null;
47
+ const jwksUrl = resolveJwksUrl(opts?.jwksUrl);
48
+ if (!jwksUrl) {
49
+ warnCoreUnconfigured('verify the capability grant on this request');
50
+ return null;
51
+ }
43
52
  const expectedAud = opts?.audience ?? process.env.KEE_AGENT_AUDIENCE;
44
53
  try {
45
54
  const { payload } = await jwtVerify(token, jwksFor(jwksUrl), {
@@ -67,7 +76,11 @@ export function grantAuth(opts) {
67
76
  },
68
77
  };
69
78
  }
70
- catch {
79
+ catch (error) {
80
+ // Still returns null — an unverifiable token must not authenticate — but no
81
+ // longer silently. A wrong JWKS URL and a genuinely bad token produced the
82
+ // same nothing before, which is what made the redirect trap so expensive.
83
+ warnGrantVerifyFailed(jwksUrl, error);
71
84
  return null;
72
85
  }
73
86
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { grantAuth } from './grant-auth.js';
2
2
  export { verifyGrant, type VerifiedGrant } from './verify-grant.js';
3
- export { useKee, MemoryConflictError, RecordConflictError, RecordValidationError, type Kee, type KeeConnection, type KeeContext, type KeeError, type KeeMemory, type KeeRecords, type KeeKb, type KBHit, type KeeTools, type MemoryEntry, type MemorySearchHit, type RecordEntry, } from './client.js';
3
+ export { useKee, MemoryConflictError, RecordConflictError, RecordValidationError, type Kee, type KeeConnection, type KeeContext, type KeeError, type KeeMemory, type KeeRecords, type KeeKb, type KBHit, type KeeTools, type KeeConfig, type MemoryEntry, type MemorySearchHit, type RecordEntry, } from './client.js';
4
4
  export { keemakrToolDirectory } from './tool-directory.js';
5
5
  export { refreshGrant, REFRESH_THRESHOLD_SECONDS } from './refresh.js';
package/dist/jwks.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * There is deliberately NO compiled-in default origin.
3
+ *
4
+ * This value is a TRUST ANCHOR: whatever it names gets to decide, for every
5
+ * tenant this agent serves, which grants are genuine. It cannot come from the
6
+ * token (a forged grant would name its own JWKS and verify against it), and it
7
+ * must not come from a constant baked into a published package either — this SDK
8
+ * ships to npm and is installed by agents on dev, staging and production alike,
9
+ * so ANY single origin compiled in here is the wrong one for at least two of
10
+ * them. A default pointing at a non-production platform is the worst case of all:
11
+ * an unconfigured production agent would accept grants signed by a platform
12
+ * anyone can get an account on.
13
+ *
14
+ * So an unconfigured deployment gets `null` and verifies nothing — but LOUDLY,
15
+ * via warnCoreUnconfigured(). That was the actual defect behind "every capability
16
+ * call 401s with nothing in the log": not the absence of a default, but the
17
+ * silence. One warning naming the two variables fixes that without inventing a
18
+ * trust anchor on the operator's behalf.
19
+ */
20
+ /** `<origin>/.well-known/jwks.json`, tolerating a trailing slash. */
21
+ export declare function jwksUrlFor(origin: string): string;
22
+ /**
23
+ * Resolve core's JWKS endpoint, most specific first, or `null` when the
24
+ * deployment has said nothing about which platform it belongs to.
25
+ *
26
+ * `KEE_CORE_URL` counts as a source in its own right: the SDK already derives
27
+ * that base URL FROM the JWKS URL, and supporting only that direction meant an
28
+ * agent could be configured to reach the capability API yet unable to verify the
29
+ * grant authorizing it — two variables for one fact, with a silent 401 as the
30
+ * penalty for setting the wrong one.
31
+ */
32
+ export declare function resolveJwksUrl(explicit?: string): string | null;
33
+ export declare function jwksFor(url: string): {
34
+ (protectedHeader?: import("jose").JWSHeaderParameters, token?: import("jose").FlattenedJWSInput): Promise<import("jose").CryptoKey>;
35
+ coolingDown: boolean;
36
+ fresh: boolean;
37
+ reloading: boolean;
38
+ reload: () => Promise<void>;
39
+ jwks: () => import("jose").JSONWebKeySet | undefined;
40
+ };
41
+ export declare function warnGrantVerifyFailed(jwksUrl: string, error: unknown): void;
42
+ export declare function warnCoreUnconfigured(attempting: string): void;
package/dist/jwks.js ADDED
@@ -0,0 +1,97 @@
1
+ // Where a keemakr agent looks for core's signing keys, and the one cache of them.
2
+ //
3
+ // Shared by both verification paths — the inbound channel auth (grant-auth) and
4
+ // the standalone machine-grant check (verify-grant) — because a deployment that
5
+ // can verify one and not the other is a configuration bug waiting to be
6
+ // diagnosed twice.
7
+ import { createRemoteJWKSet } from 'jose';
8
+ /**
9
+ * There is deliberately NO compiled-in default origin.
10
+ *
11
+ * This value is a TRUST ANCHOR: whatever it names gets to decide, for every
12
+ * tenant this agent serves, which grants are genuine. It cannot come from the
13
+ * token (a forged grant would name its own JWKS and verify against it), and it
14
+ * must not come from a constant baked into a published package either — this SDK
15
+ * ships to npm and is installed by agents on dev, staging and production alike,
16
+ * so ANY single origin compiled in here is the wrong one for at least two of
17
+ * them. A default pointing at a non-production platform is the worst case of all:
18
+ * an unconfigured production agent would accept grants signed by a platform
19
+ * anyone can get an account on.
20
+ *
21
+ * So an unconfigured deployment gets `null` and verifies nothing — but LOUDLY,
22
+ * via warnCoreUnconfigured(). That was the actual defect behind "every capability
23
+ * call 401s with nothing in the log": not the absence of a default, but the
24
+ * silence. One warning naming the two variables fixes that without inventing a
25
+ * trust anchor on the operator's behalf.
26
+ */
27
+ /** `<origin>/.well-known/jwks.json`, tolerating a trailing slash. */
28
+ export function jwksUrlFor(origin) {
29
+ return `${origin.replace(/\/$/, '')}/.well-known/jwks.json`;
30
+ }
31
+ /**
32
+ * Resolve core's JWKS endpoint, most specific first, or `null` when the
33
+ * deployment has said nothing about which platform it belongs to.
34
+ *
35
+ * `KEE_CORE_URL` counts as a source in its own right: the SDK already derives
36
+ * that base URL FROM the JWKS URL, and supporting only that direction meant an
37
+ * agent could be configured to reach the capability API yet unable to verify the
38
+ * grant authorizing it — two variables for one fact, with a silent 401 as the
39
+ * penalty for setting the wrong one.
40
+ */
41
+ export function resolveJwksUrl(explicit) {
42
+ if (explicit)
43
+ return explicit;
44
+ if (process.env.KEE_CORE_JWKS_URL)
45
+ return process.env.KEE_CORE_JWKS_URL;
46
+ if (process.env.KEE_CORE_URL)
47
+ return jwksUrlFor(process.env.KEE_CORE_URL);
48
+ return null;
49
+ }
50
+ // Keyed by URL. The previous single-slot cache handed back the FIRST keyset it
51
+ // ever built for every later call, so a process verifying against two cores used
52
+ // the wrong keys for one of them and reported nothing.
53
+ const jwksCache = new Map();
54
+ export function jwksFor(url) {
55
+ let set = jwksCache.get(url);
56
+ if (!set) {
57
+ set = createRemoteJWKSet(new URL(url));
58
+ jwksCache.set(url, set);
59
+ }
60
+ return set;
61
+ }
62
+ // A misconfigured JWKS URL was indistinguishable from an unauthorized caller: the
63
+ // verify threw, the caller returned null, and every capability call 401'd with
64
+ // nothing said about why. The classic cause is an origin that REDIRECTS (e.g.
65
+ // app.dev.keemakr.ai → dash.dev.keemakr.ai), where `jose` fetches HTML and finds
66
+ // no keys. Warn once per URL: enough to diagnose, not enough to flood a hot path.
67
+ const warned = new Set();
68
+ export function warnGrantVerifyFailed(jwksUrl, error) {
69
+ if (warned.has(jwksUrl))
70
+ return;
71
+ warned.add(jwksUrl);
72
+ const reason = error instanceof Error ? error.message : String(error);
73
+ console.warn(`[keemakr] could not verify a capability grant against ${jwksUrl}: ${reason}. ` +
74
+ 'If that URL redirects or serves no keys, set KEE_CORE_JWKS_URL (or KEE_CORE_URL) ' +
75
+ 'to the origin serving /.well-known/jwks.json for the platform you installed on.');
76
+ }
77
+ /**
78
+ * The other half of the same diagnosis problem: a deployment that named no
79
+ * platform at all. It still refuses the token — an agent that does not know whose
80
+ * keys to trust must not decide that a grant is genuine — but it now says so
81
+ * instead of returning the same silent null a genuinely bad token gets.
82
+ *
83
+ * Warned once per process (not per request): this fires on a hot path, and after
84
+ * the first line the operator has everything they need.
85
+ */
86
+ let warnedUnconfigured = false;
87
+ export function warnCoreUnconfigured(attempting) {
88
+ if (warnedUnconfigured)
89
+ return;
90
+ warnedUnconfigured = true;
91
+ console.warn(`[keemakr] cannot ${attempting}: this deployment has not been told which keemakr ` +
92
+ 'platform it belongs to, so there are no signing keys to trust and no capability ' +
93
+ 'API to call. Set KEE_CORE_URL to the origin you installed on (e.g. ' +
94
+ 'https://dash.<env>.keemakr.ai), or KEE_CORE_JWKS_URL to its ' +
95
+ '/.well-known/jwks.json directly. There is no default on purpose — guessing ' +
96
+ 'would mean trusting a platform you did not choose.');
97
+ }
package/dist/refresh.d.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  /** Refresh when the active token has less than this long left to live. */
2
2
  export declare const REFRESH_THRESHOLD_SECONDS = 120;
3
- /** Resolve core's base URL: KEE_CORE_URL, else derived from KEE_CORE_JWKS_URL. */
3
+ /**
4
+ * Resolve core's base URL: KEE_CORE_URL, else the origin of KEE_CORE_JWKS_URL.
5
+ *
6
+ * Throws when neither is set, and that is the safe answer rather than the
7
+ * unhelpful one. This URL is where the SDK POSTs the tenant's grant token, so a
8
+ * guessed default is not a convenience — it is a live credential sent to a host
9
+ * nobody chose, which on an unconfigured production agent would mean handing a
10
+ * production grant to whatever environment the constant happened to name. Failing
11
+ * at the first capability call, with both variable names in the message, costs one
12
+ * deploy; the other failure mode costs a credential.
13
+ */
4
14
  export declare function coreBaseUrl(): string;
5
15
  /** The freshest token known for a delegation (the exchanged one, else the original). */
6
16
  export declare function activeGrantToken(originalToken: string): string;
package/dist/refresh.js CHANGED
@@ -19,7 +19,17 @@ export const REFRESH_THRESHOLD_SECONDS = 120;
19
19
  const refreshed = new Map();
20
20
  // original grant token → in-flight refresh, so N concurrent tool calls share one POST.
21
21
  const inflight = new Map();
22
- /** Resolve core's base URL: KEE_CORE_URL, else derived from KEE_CORE_JWKS_URL. */
22
+ /**
23
+ * Resolve core's base URL: KEE_CORE_URL, else the origin of KEE_CORE_JWKS_URL.
24
+ *
25
+ * Throws when neither is set, and that is the safe answer rather than the
26
+ * unhelpful one. This URL is where the SDK POSTs the tenant's grant token, so a
27
+ * guessed default is not a convenience — it is a live credential sent to a host
28
+ * nobody chose, which on an unconfigured production agent would mean handing a
29
+ * production grant to whatever environment the constant happened to name. Failing
30
+ * at the first capability call, with both variable names in the message, costs one
31
+ * deploy; the other failure mode costs a credential.
32
+ */
23
33
  export function coreBaseUrl() {
24
34
  const explicit = process.env.KEE_CORE_URL;
25
35
  if (explicit)
@@ -27,7 +37,10 @@ export function coreBaseUrl() {
27
37
  const jwks = process.env.KEE_CORE_JWKS_URL;
28
38
  if (jwks)
29
39
  return jwks.replace(/\/\.well-known\/jwks\.json\/?$/, '');
30
- const e = new Error('KEE_CORE_URL (or KEE_CORE_JWKS_URL) must be set to reach the Capability API');
40
+ const e = new Error('KEE_CORE_URL (or KEE_CORE_JWKS_URL) must be set to reach the Capability API' +
41
+ 'it names the keemakr platform this agent was installed on, e.g. ' +
42
+ 'https://dash.<env>.keemakr.ai. There is no default: the grant token is sent ' +
43
+ 'to this URL, so guessing would leak it to a platform you did not choose.');
31
44
  e.name = 'KeeError';
32
45
  throw e;
33
46
  }
@@ -10,9 +10,11 @@ export interface VerifiedGrant {
10
10
  /**
11
11
  * Verify a capability grant (session OR machine) against keemakr-core's JWKS.
12
12
  * Returns the trusted claims, or `null` on any failure (bad signature, wrong
13
- * issuer/audience, expired). Both `jwksUrl` and `audience` fall back to
14
- * KEE_CORE_JWKS_URL / KEE_AGENT_AUDIENCE. `audience` is strongly recommended: a
15
- * grant is only valid for the remote it was minted for.
13
+ * issuer/audience, expired). `jwksUrl` falls back to KEE_CORE_JWKS_URL, then to
14
+ * KEE_CORE_URL's origin and to nothing after that, so a deployment that has not
15
+ * named its platform refuses every grant rather than trusting a compiled-in
16
+ * default it did not choose. `audience` falls back to KEE_AGENT_AUDIENCE and is
17
+ * strongly recommended: a grant is only valid for the remote it was minted for.
16
18
  */
17
19
  export declare function verifyGrant(token: string, opts?: {
18
20
  jwksUrl?: string;
@@ -10,25 +10,24 @@
10
10
  // A machine grant is the SAME token shape as a session grant (same issuer, aud,
11
11
  // tenant_id, scopes) — only the TTL and the mint path differ — so this one
12
12
  // verifier covers both.
13
- import { createRemoteJWKSet, jwtVerify } from 'jose';
13
+ import { jwtVerify } from 'jose';
14
+ import { jwksFor, resolveJwksUrl, warnCoreUnconfigured, warnGrantVerifyFailed } from './jwks.js';
14
15
  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
16
  /**
22
17
  * Verify a capability grant (session OR machine) against keemakr-core's JWKS.
23
18
  * Returns the trusted claims, or `null` on any failure (bad signature, wrong
24
- * issuer/audience, expired). Both `jwksUrl` and `audience` fall back to
25
- * KEE_CORE_JWKS_URL / KEE_AGENT_AUDIENCE. `audience` is strongly recommended: a
26
- * grant is only valid for the remote it was minted for.
19
+ * issuer/audience, expired). `jwksUrl` falls back to KEE_CORE_JWKS_URL, then to
20
+ * KEE_CORE_URL's origin and to nothing after that, so a deployment that has not
21
+ * named its platform refuses every grant rather than trusting a compiled-in
22
+ * default it did not choose. `audience` falls back to KEE_AGENT_AUDIENCE and is
23
+ * strongly recommended: a grant is only valid for the remote it was minted for.
27
24
  */
28
25
  export async function verifyGrant(token, opts) {
29
- const jwksUrl = opts?.jwksUrl ?? process.env.KEE_CORE_JWKS_URL;
30
- if (!jwksUrl)
26
+ const jwksUrl = resolveJwksUrl(opts?.jwksUrl);
27
+ if (!jwksUrl) {
28
+ warnCoreUnconfigured('verify this capability grant');
31
29
  return null;
30
+ }
32
31
  const expectedAud = opts?.audience ?? process.env.KEE_AGENT_AUDIENCE;
33
32
  try {
34
33
  const { payload } = await jwtVerify(token, jwksFor(jwksUrl), {
@@ -46,7 +45,10 @@ export async function verifyGrant(token, opts) {
46
45
  exp: typeof payload.exp === 'number' ? payload.exp : null,
47
46
  };
48
47
  }
49
- catch {
48
+ catch (error) {
49
+ // Null either way — an unverifiable grant must not pass — but say why once, so
50
+ // a misconfigured JWKS URL is distinguishable from a genuinely bad token.
51
+ warnGrantVerifyFailed(jwksUrl, error);
50
52
  return null;
51
53
  }
52
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keemakr/agent-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "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.",
5
5
  "license": "MIT",
6
6
  "type": "module",