@keemakr/agent-sdk 0.16.0 → 0.17.0-dev-20260928074313
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/dist/channel-testkit.d.ts +57 -0
- package/dist/channel-testkit.js +136 -0
- package/dist/channel.d.ts +169 -0
- package/dist/channel.js +359 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/verify-grant.d.ts +0 -9
- package/dist/verify-grant.js +23 -0
- package/package.json +14 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ChannelAction } from './channel.js';
|
|
2
|
+
export interface KbFixture {
|
|
3
|
+
chunkId: string;
|
|
4
|
+
title: string | null;
|
|
5
|
+
text: string;
|
|
6
|
+
score?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface FakeCoreOptions {
|
|
9
|
+
/** What `ctx.kb.search()` returns. A function, so a fixture can vary by query. */
|
|
10
|
+
kb?: KbFixture[] | ((query: string) => KbFixture[]);
|
|
11
|
+
/** What `ctx.history()` returns. */
|
|
12
|
+
history?: Array<{
|
|
13
|
+
body: string;
|
|
14
|
+
authorKind: string;
|
|
15
|
+
position: number;
|
|
16
|
+
}>;
|
|
17
|
+
tenantId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Delivered {
|
|
20
|
+
/** What the pack posted back, or null if it never did. */
|
|
21
|
+
action: (ChannelAction & Record<string, unknown>) | null;
|
|
22
|
+
/** The HTTP status the pack answered the delivery with. */
|
|
23
|
+
status: number;
|
|
24
|
+
}
|
|
25
|
+
export interface FakeCore {
|
|
26
|
+
/** Point the pack here: its `coreUrl`, and the issuer of every grant below. */
|
|
27
|
+
url: string;
|
|
28
|
+
/** And here: the audience every minted grant will name. */
|
|
29
|
+
audienceFor(packUrl: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Deliver a customer message to a pack and wait for its answer.
|
|
32
|
+
*
|
|
33
|
+
* Waits for the POST-BACK rather than for the 202, because the 202 means only that the
|
|
34
|
+
* pack accepted the work. A test that asserted on the acknowledgement would pass against
|
|
35
|
+
* a pack that never answers — which is the single commonest way a first pack is broken.
|
|
36
|
+
*/
|
|
37
|
+
deliver(input: {
|
|
38
|
+
to: string;
|
|
39
|
+
text: string | string[];
|
|
40
|
+
conversationId?: string;
|
|
41
|
+
deliveryId?: string;
|
|
42
|
+
contact?: {
|
|
43
|
+
id: string;
|
|
44
|
+
handle: string | null;
|
|
45
|
+
name: string | null;
|
|
46
|
+
asserted_name?: string | null;
|
|
47
|
+
};
|
|
48
|
+
binding?: Record<string, unknown>;
|
|
49
|
+
language?: {
|
|
50
|
+
customer: string | null;
|
|
51
|
+
};
|
|
52
|
+
contract?: number;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
}): Promise<Delivered>;
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare function startFakeCore(options?: FakeCoreOptions): Promise<FakeCore>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// A core that is not running, for proving a pack before anything is deployed.
|
|
2
|
+
//
|
|
3
|
+
// WHAT THIS IS FOR. Today the shortest path to knowing whether a pack works is: deploy it,
|
|
4
|
+
// publish an entry, install it on a tenant, bind it to a channel, send a real message, and
|
|
5
|
+
// then read the database. Every one of those steps can fail for reasons that have nothing
|
|
6
|
+
// to do with the pack, and the failure looks the same from the author's side — core's own
|
|
7
|
+
// e2e script says so in its comments: "a dispatch that never reached it and an answer that
|
|
8
|
+
// never came back look identical from here".
|
|
9
|
+
//
|
|
10
|
+
// So this mints grants with a real keypair, serves the three endpoints a pack actually
|
|
11
|
+
// calls, receives the post-back, and hands it to you. A pack test becomes: deliver this,
|
|
12
|
+
// assert that.
|
|
13
|
+
//
|
|
14
|
+
// IT REFUSES TO RUN IN PRODUCTION. It mints grants that its own JWKS validates, which is
|
|
15
|
+
// the whole point in a test and a way to accept forged grants anywhere else.
|
|
16
|
+
import { createServer } from 'node:http';
|
|
17
|
+
import { exportJWK, generateKeyPair, SignJWT } from 'jose';
|
|
18
|
+
const UUID_NS = '00000000-0000-4000-8000-0000000000';
|
|
19
|
+
export async function startFakeCore(options = {}) {
|
|
20
|
+
if (process.env.NODE_ENV === 'production') {
|
|
21
|
+
throw new Error('[keemakr] startFakeCore mints grants its own JWKS validates. That is the point in a ' +
|
|
22
|
+
'test and a way to accept forged grants anywhere else — it refuses to run in production.');
|
|
23
|
+
}
|
|
24
|
+
const tenantId = options.tenantId ?? `${UUID_NS}01`;
|
|
25
|
+
const { publicKey, privateKey } = await generateKeyPair('RS256');
|
|
26
|
+
const jwk = { ...(await exportJWK(publicKey)), kid: 'testkit', alg: 'RS256', use: 'sig' };
|
|
27
|
+
/** Resolved when a pack posts its answer back for a given delivery. */
|
|
28
|
+
const waiting = new Map();
|
|
29
|
+
const server = createServer((req, res) => {
|
|
30
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
31
|
+
const json = (status, body) => {
|
|
32
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
33
|
+
res.end(JSON.stringify(body));
|
|
34
|
+
};
|
|
35
|
+
if (url.pathname === '/.well-known/jwks.json')
|
|
36
|
+
return json(200, { keys: [jwk] });
|
|
37
|
+
if (url.pathname === '/api/capability/kb/retrieve') {
|
|
38
|
+
let raw = '';
|
|
39
|
+
req.on('data', (c) => (raw += c));
|
|
40
|
+
req.on('end', () => {
|
|
41
|
+
const query = JSON.parse(raw || '{}').query ?? '';
|
|
42
|
+
const hits = typeof options.kb === 'function' ? options.kb(query) : (options.kb ?? []);
|
|
43
|
+
json(200, {
|
|
44
|
+
hits: hits.map((h) => ({
|
|
45
|
+
chunk_id: h.chunkId,
|
|
46
|
+
title: h.title,
|
|
47
|
+
text: h.text,
|
|
48
|
+
score: h.score ?? 0.5,
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (url.pathname.endsWith('/actions')) {
|
|
55
|
+
let raw = '';
|
|
56
|
+
req.on('data', (c) => (raw += c));
|
|
57
|
+
req.on('end', () => {
|
|
58
|
+
const action = JSON.parse(raw || '{}');
|
|
59
|
+
const resolve = waiting.get(String(action.delivery_id));
|
|
60
|
+
if (resolve) {
|
|
61
|
+
waiting.delete(String(action.delivery_id));
|
|
62
|
+
resolve(action);
|
|
63
|
+
}
|
|
64
|
+
json(200, { ok: true });
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Everything else is the history read.
|
|
69
|
+
return json(200, { messages: options.history ?? [] });
|
|
70
|
+
});
|
|
71
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
72
|
+
const port = server.address().port;
|
|
73
|
+
const url = `http://127.0.0.1:${port}`;
|
|
74
|
+
async function mint(conversationId, audience) {
|
|
75
|
+
return new SignJWT({
|
|
76
|
+
tenant_id: tenantId,
|
|
77
|
+
channel_conversation_id: conversationId,
|
|
78
|
+
// The REAL ceiling, not a permissive one. A testkit that minted broader scopes than
|
|
79
|
+
// core does would let a pack pass its own tests while calling something production
|
|
80
|
+
// refuses — which is worse than no test, because it is a test that lies.
|
|
81
|
+
scopes: ['channel:history', 'channel:respond', 'kb:retrieve'],
|
|
82
|
+
external_kee: true,
|
|
83
|
+
trace_id: `testkit-${Date.now()}`,
|
|
84
|
+
})
|
|
85
|
+
.setProtectedHeader({ alg: 'RS256', kid: 'testkit' })
|
|
86
|
+
.setIssuer('keemakr')
|
|
87
|
+
.setAudience(audience)
|
|
88
|
+
.setExpirationTime('5m')
|
|
89
|
+
.sign(privateKey);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
url,
|
|
93
|
+
audienceFor(packUrl) {
|
|
94
|
+
return new URL(packUrl).origin;
|
|
95
|
+
},
|
|
96
|
+
async deliver(input) {
|
|
97
|
+
const conversationId = input.conversationId ?? `${UUID_NS}c1`;
|
|
98
|
+
const deliveryId = input.deliveryId ?? `${UUID_NS}d${Math.floor(Math.random() * 90 + 10)}`;
|
|
99
|
+
const bodies = Array.isArray(input.text) ? input.text : [input.text];
|
|
100
|
+
const token = await mint(conversationId, new URL(input.to).origin);
|
|
101
|
+
const answered = new Promise((resolve) => {
|
|
102
|
+
waiting.set(deliveryId, resolve);
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
if (waiting.delete(deliveryId))
|
|
105
|
+
resolve(null);
|
|
106
|
+
}, input.timeoutMs ?? 10_000).unref?.();
|
|
107
|
+
});
|
|
108
|
+
const res = await fetch(input.to, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
authorization: `Bearer ${token}`,
|
|
112
|
+
'content-type': 'application/json',
|
|
113
|
+
'idempotency-key': deliveryId,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
event: 'message.received',
|
|
117
|
+
delivery_id: deliveryId,
|
|
118
|
+
conversation: { id: conversationId },
|
|
119
|
+
contact: input.contact ?? { id: `${UUID_NS}a1`, handle: 'v_test', name: null },
|
|
120
|
+
unanswered_messages: bodies.map((body, i) => ({ body, position: i + 1 })),
|
|
121
|
+
truncated_messages: 0,
|
|
122
|
+
...(input.contract !== undefined ? { contract: input.contract } : {}),
|
|
123
|
+
...(input.binding ? { binding: input.binding } : {}),
|
|
124
|
+
...(input.language ? { language: input.language } : {}),
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
status: res.status,
|
|
129
|
+
action: (await answered),
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
async close() {
|
|
133
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** The envelope core POSTs. Loose by rule — core adds fields without asking packs first. */
|
|
2
|
+
export interface ChannelDelivery {
|
|
3
|
+
event: string;
|
|
4
|
+
delivery_id: string;
|
|
5
|
+
conversation: {
|
|
6
|
+
id: string;
|
|
7
|
+
};
|
|
8
|
+
/** `asserted_name` is a name the SENDER typed; `name` is one the provider vouches for. */
|
|
9
|
+
contact: {
|
|
10
|
+
id: string;
|
|
11
|
+
handle: string | null;
|
|
12
|
+
name: string | null;
|
|
13
|
+
asserted_name?: string | null;
|
|
14
|
+
};
|
|
15
|
+
unanswered_messages: Array<{
|
|
16
|
+
body: string;
|
|
17
|
+
position: number;
|
|
18
|
+
}>;
|
|
19
|
+
truncated_messages: number;
|
|
20
|
+
contract?: number;
|
|
21
|
+
binding?: {
|
|
22
|
+
id: string;
|
|
23
|
+
autonomy: 'supervised' | 'assisted' | 'autonomous';
|
|
24
|
+
settings: Record<string, unknown>;
|
|
25
|
+
settings_version: number;
|
|
26
|
+
};
|
|
27
|
+
language?: {
|
|
28
|
+
customer: string | null;
|
|
29
|
+
};
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
/** What a pack proposes. Core decides what happens to it. */
|
|
33
|
+
export type ChannelAction = {
|
|
34
|
+
action: 'reply';
|
|
35
|
+
text: string;
|
|
36
|
+
confidence?: number;
|
|
37
|
+
grounded_on?: string[];
|
|
38
|
+
usage: Usage;
|
|
39
|
+
} | {
|
|
40
|
+
action: 'handoff';
|
|
41
|
+
reason: HandoffReason;
|
|
42
|
+
usage: Usage;
|
|
43
|
+
} | {
|
|
44
|
+
action: 'none';
|
|
45
|
+
usage: Usage;
|
|
46
|
+
};
|
|
47
|
+
export type HandoffReason = 'customer_requested' | 'no_answer' | 'agent_failed';
|
|
48
|
+
export interface Usage {
|
|
49
|
+
input_tokens: number;
|
|
50
|
+
output_tokens: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ChannelContext {
|
|
53
|
+
tenantId: string;
|
|
54
|
+
conversationId: string;
|
|
55
|
+
deliveryId: string;
|
|
56
|
+
/** THE WHOLE UNANSWERED RUN. A customer who split a question across three bubbles asked
|
|
57
|
+
* one question, and a pack handed only the newest line answers the fragment. */
|
|
58
|
+
messages: Array<{
|
|
59
|
+
body: string;
|
|
60
|
+
position: number;
|
|
61
|
+
}>;
|
|
62
|
+
/** How many OLDER messages did not fit the tenant's budget. Said, not hidden. */
|
|
63
|
+
truncated: number;
|
|
64
|
+
/**
|
|
65
|
+
* Who is being talked to.
|
|
66
|
+
*
|
|
67
|
+
* `name` is a profile the provider vouches for. `asserted_name` is a string the sender
|
|
68
|
+
* typed into a form on a public website, and a widget delivery carries only the second —
|
|
69
|
+
* treat it as untrusted input wherever it reaches a prompt.
|
|
70
|
+
*/
|
|
71
|
+
contact: {
|
|
72
|
+
id: string;
|
|
73
|
+
handle: string | null;
|
|
74
|
+
name: string | null;
|
|
75
|
+
asserted_name?: string | null;
|
|
76
|
+
};
|
|
77
|
+
/** What this business wants its agent to be. Absent against a core that predates it. */
|
|
78
|
+
binding?: ChannelDelivery['binding'];
|
|
79
|
+
/** The customer's apparent language, or null when core looked and could not say. */
|
|
80
|
+
language?: {
|
|
81
|
+
customer: string | null;
|
|
82
|
+
};
|
|
83
|
+
/** Earlier turns in this conversation, under `channel:history`. */
|
|
84
|
+
history(limit?: number): Promise<Array<{
|
|
85
|
+
body: string;
|
|
86
|
+
authorKind: string;
|
|
87
|
+
position: number;
|
|
88
|
+
}>>;
|
|
89
|
+
/** This tenant's customer-safe knowledge, under `kb:retrieve`. */
|
|
90
|
+
kb: {
|
|
91
|
+
search(query: string, opts?: {
|
|
92
|
+
limit?: number;
|
|
93
|
+
}): Promise<Array<{
|
|
94
|
+
chunkId: string;
|
|
95
|
+
title: string | null;
|
|
96
|
+
text: string;
|
|
97
|
+
score: number;
|
|
98
|
+
}>>;
|
|
99
|
+
};
|
|
100
|
+
/** Pure builders. They construct an action; they do not send anything. */
|
|
101
|
+
reply(text: string, opts?: {
|
|
102
|
+
confidence?: number;
|
|
103
|
+
groundedOn?: string[];
|
|
104
|
+
usage?: Usage;
|
|
105
|
+
}): ChannelAction;
|
|
106
|
+
handoff(reason: HandoffReason, opts?: {
|
|
107
|
+
usage?: Usage;
|
|
108
|
+
}): ChannelAction;
|
|
109
|
+
none(opts?: {
|
|
110
|
+
usage?: Usage;
|
|
111
|
+
}): ChannelAction;
|
|
112
|
+
}
|
|
113
|
+
export interface ChannelAgentOptions {
|
|
114
|
+
/**
|
|
115
|
+
* The core this pack answers for.
|
|
116
|
+
*
|
|
117
|
+
* Everything trusted flows from here: the JWKS that proves a grant is real, and the API
|
|
118
|
+
* the answer is posted back to. A wrong value is not a broken pack — it is a pack that
|
|
119
|
+
* trusts somebody else's signatures, so it is validated at construction rather than at
|
|
120
|
+
* the first request.
|
|
121
|
+
*/
|
|
122
|
+
coreUrl?: string;
|
|
123
|
+
/**
|
|
124
|
+
* The audience every grant must name, and it is REQUIRED here even though `verifyGrant`
|
|
125
|
+
* tolerates its absence.
|
|
126
|
+
*
|
|
127
|
+
* Without it a grant minted for any other deployment of any other pack verifies against
|
|
128
|
+
* this one. `verifyGrant` keeps the lenient behaviour because packs depend on it today;
|
|
129
|
+
* a helper being written for the first time has no such excuse.
|
|
130
|
+
*/
|
|
131
|
+
audience: string;
|
|
132
|
+
/** The one function a pack author writes. */
|
|
133
|
+
answer: (ctx: ChannelContext) => Promise<ChannelAction> | ChannelAction;
|
|
134
|
+
/** The platform's keep-alive. On Vercel: `waitUntil` from `@vercel/functions`. */
|
|
135
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
136
|
+
/** Injected for tests. Defaults to global fetch. */
|
|
137
|
+
fetchImpl?: typeof fetch;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Refuse a core URL that cannot be trusted, at construction.
|
|
141
|
+
*
|
|
142
|
+
* The JWKS URL is derived from this with no scheme check anywhere below, so a pack pointed
|
|
143
|
+
* at `http://` fetches its trust anchor over a connection anybody on the path can rewrite —
|
|
144
|
+
* which means anybody on the path can mint grants it will accept. Loopback is exempted
|
|
145
|
+
* because local development is the reason the check is survivable at all, and loopback is
|
|
146
|
+
* then refused under NODE_ENV=production because "it worked on my machine" is precisely how
|
|
147
|
+
* that exemption reaches a deployment.
|
|
148
|
+
*/
|
|
149
|
+
export declare function assertTrustworthyCore(raw: string): URL;
|
|
150
|
+
export interface ChannelAgent {
|
|
151
|
+
/** For a fetch-style platform (Vercel, Workers, Bun, Deno). */
|
|
152
|
+
fetchHandler(req: Request, ctx?: {
|
|
153
|
+
waitUntil?: (p: Promise<unknown>) => void;
|
|
154
|
+
}): Promise<Response>;
|
|
155
|
+
/**
|
|
156
|
+
* The same answer, computed INLINE and returned rather than posted back.
|
|
157
|
+
*
|
|
158
|
+
* WHY A SEPARATE HANDLER AND NOT A FLAG. The delivery path is asynchronous because a real
|
|
159
|
+
* customer's turn must survive the pack being slow; a preview is somebody staring at a
|
|
160
|
+
* settings drawer, where an answer that arrives on a later request is no answer at all.
|
|
161
|
+
* Same verification, same context, same `answer` — only the ending differs.
|
|
162
|
+
*
|
|
163
|
+
* It writes nothing. The grant core mints for this names a conversation that does not
|
|
164
|
+
* exist, so the post-back route would refuse it anyway; returning the proposal is the
|
|
165
|
+
* honest shape rather than a side effect somebody has to remember to suppress.
|
|
166
|
+
*/
|
|
167
|
+
previewHandler(req: Request): Promise<Response>;
|
|
168
|
+
}
|
|
169
|
+
export declare function createChannelAgent(options: ChannelAgentOptions): ChannelAgent;
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// Answering a customer on a channel, without writing the security-critical half yourself.
|
|
2
|
+
//
|
|
3
|
+
// WHAT THIS REPLACES. The reference pack hand-rolled seventy-eight lines of grant
|
|
4
|
+
// verification, because `verifyGrant` drops the one claim a channel turn depends on — the
|
|
5
|
+
// conversation this grant is bound to — and it parsed an envelope that existed only as an
|
|
6
|
+
// inline object literal inside core. Every pack after it would have copied that file, and a
|
|
7
|
+
// copy of security-critical code is a copy that stops being reviewed.
|
|
8
|
+
//
|
|
9
|
+
// THE SHAPE OF THE CONTRACT, which is not obvious and is the thing most first packs get
|
|
10
|
+
// wrong: core POSTs a delivery, the pack ACKNOWLEDGES within seconds, and the answer comes
|
|
11
|
+
// back later on a SEPARATE request. Holding the connection open for the whole think is what
|
|
12
|
+
// a first implementation does and what a serverless platform kills. So `answer` runs after
|
|
13
|
+
// the response, inside the platform's `waitUntil`, and the helper posts the result back.
|
|
14
|
+
//
|
|
15
|
+
// `answer` RETURNS its action rather than calling something. That makes "exactly one
|
|
16
|
+
// proposal per delivery" a property of the type rather than of everybody remembering, and
|
|
17
|
+
// it makes `answer` a pure function that can be unit-tested with no core at all — fake or
|
|
18
|
+
// otherwise.
|
|
19
|
+
import { createRemoteJWKSet, customFetch, jwtVerify } from 'jose';
|
|
20
|
+
import { jwksUrlFor, resolveJwksUrl } from './jwks.js';
|
|
21
|
+
const GRANT_ISSUER = 'keemakr';
|
|
22
|
+
/** The only delivery-envelope version this SDK speaks. */
|
|
23
|
+
const SUPPORTED_CONTRACT = 1;
|
|
24
|
+
/** How long a delivery id is remembered, so a core retry does not pay for a second think. */
|
|
25
|
+
const SEEN_TTL_MS = 90_000;
|
|
26
|
+
const SEEN_MAX = 500;
|
|
27
|
+
/**
|
|
28
|
+
* Refuse a core URL that cannot be trusted, at construction.
|
|
29
|
+
*
|
|
30
|
+
* The JWKS URL is derived from this with no scheme check anywhere below, so a pack pointed
|
|
31
|
+
* at `http://` fetches its trust anchor over a connection anybody on the path can rewrite —
|
|
32
|
+
* which means anybody on the path can mint grants it will accept. Loopback is exempted
|
|
33
|
+
* because local development is the reason the check is survivable at all, and loopback is
|
|
34
|
+
* then refused under NODE_ENV=production because "it worked on my machine" is precisely how
|
|
35
|
+
* that exemption reaches a deployment.
|
|
36
|
+
*/
|
|
37
|
+
export function assertTrustworthyCore(raw) {
|
|
38
|
+
let url;
|
|
39
|
+
try {
|
|
40
|
+
url = new URL(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`[keemakr] KEE_CORE_URL is not a URL: ${raw}`);
|
|
44
|
+
}
|
|
45
|
+
const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]';
|
|
46
|
+
if (url.protocol !== 'https:' && !loopback) {
|
|
47
|
+
throw new Error(`[keemakr] refusing to trust ${url.origin} over ${url.protocol} — the JWKS fetched ` +
|
|
48
|
+
'from it is what proves a grant is real, so anybody on the path could mint grants ' +
|
|
49
|
+
'this pack accepts. Use https, or localhost for development.');
|
|
50
|
+
}
|
|
51
|
+
if (loopback && process.env.NODE_ENV === 'production') {
|
|
52
|
+
throw new Error(`[keemakr] refusing to trust ${url.origin} in production — a deployed pack pointed at ` +
|
|
53
|
+
'loopback verifies nothing.');
|
|
54
|
+
}
|
|
55
|
+
return url;
|
|
56
|
+
}
|
|
57
|
+
/** Verify, keeping the claim `verifyGrant` drops. */
|
|
58
|
+
async function verifyChannelGrant(token, keys, audience) {
|
|
59
|
+
try {
|
|
60
|
+
const { payload } = await jwtVerify(token, keys, {
|
|
61
|
+
issuer: GRANT_ISSUER,
|
|
62
|
+
audience,
|
|
63
|
+
});
|
|
64
|
+
if (typeof payload.tenant_id !== 'string' || !Array.isArray(payload.scopes))
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
tenantId: payload.tenant_id,
|
|
68
|
+
// THE CLAIM THE WHOLE LANE RESTS ON, and the one `verifyGrant` throws away. Without
|
|
69
|
+
// it a live grant can post an answer into ANY conversation in the tenant for its
|
|
70
|
+
// whole life — Idempotency-Key is deduplication, not authorization.
|
|
71
|
+
conversationId: typeof payload.channel_conversation_id === 'string'
|
|
72
|
+
? payload.channel_conversation_id
|
|
73
|
+
: null,
|
|
74
|
+
scopes: payload.scopes.map(String),
|
|
75
|
+
traceId: typeof payload.trace_id === 'string' ? payload.trace_id : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Remembered deliveries, so core's retry does not buy a second model call. */
|
|
83
|
+
class SeenDeliveries {
|
|
84
|
+
seen = new Map();
|
|
85
|
+
/** True if this is new. Keyed on VERIFIED ids only — see the note at the call site. */
|
|
86
|
+
admit(key, now) {
|
|
87
|
+
for (const [k, at] of this.seen) {
|
|
88
|
+
if (now - at > SEEN_TTL_MS)
|
|
89
|
+
this.seen.delete(k);
|
|
90
|
+
}
|
|
91
|
+
if (this.seen.has(key))
|
|
92
|
+
return false;
|
|
93
|
+
if (this.seen.size >= SEEN_MAX) {
|
|
94
|
+
const oldest = this.seen.keys().next().value;
|
|
95
|
+
if (oldest !== undefined)
|
|
96
|
+
this.seen.delete(oldest);
|
|
97
|
+
}
|
|
98
|
+
this.seen.set(key, now);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
forget(key) {
|
|
102
|
+
this.seen.delete(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function createChannelAgent(options) {
|
|
106
|
+
if (!options.audience?.trim()) {
|
|
107
|
+
// AT CONSTRUCTION, not at the first delivery. A pack that boots and then refuses every
|
|
108
|
+
// customer looks like a broken product; a pack that refuses to boot looks like a
|
|
109
|
+
// missing environment variable, which is what it is.
|
|
110
|
+
throw new Error("[keemakr] createChannelAgent needs an `audience`: this deployment's exact origin, " +
|
|
111
|
+
"the same URL registered as the entry's runtime. Without it a grant minted for any " +
|
|
112
|
+
'other pack verifies here.');
|
|
113
|
+
}
|
|
114
|
+
const coreRaw = options.coreUrl ?? process.env.KEE_CORE_URL;
|
|
115
|
+
if (!coreRaw) {
|
|
116
|
+
throw new Error('[keemakr] createChannelAgent needs `coreUrl` or KEE_CORE_URL.');
|
|
117
|
+
}
|
|
118
|
+
const core = assertTrustworthyCore(coreRaw);
|
|
119
|
+
const jwksUrl = resolveJwksUrl() ?? jwksUrlFor(core.origin);
|
|
120
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
121
|
+
// The keyset fetches through the SAME fetch as everything else. Its own cache is what
|
|
122
|
+
// makes this one round trip per key rotation rather than one per delivery, and routing it
|
|
123
|
+
// through the injected fetch is what lets a test — or a pack behind a proxy — serve it.
|
|
124
|
+
const keys = createRemoteJWKSet(new URL(jwksUrl), { [customFetch]: fetchImpl });
|
|
125
|
+
const seen = new SeenDeliveries();
|
|
126
|
+
// SAID AT BOOT, once. A pack pointed at the wrong core fails in a way that looks like a
|
|
127
|
+
// core outage from every angle except this line.
|
|
128
|
+
console.log(`[keemakr] channel agent trusting ${core.origin} (audience ${options.audience})`);
|
|
129
|
+
async function handle(req, waitUntil) {
|
|
130
|
+
const bearer = req.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ?? '';
|
|
131
|
+
if (!bearer)
|
|
132
|
+
return refuse(401, 'no_grant');
|
|
133
|
+
const claims = await verifyChannelGrant(bearer, keys, options.audience);
|
|
134
|
+
if (!claims)
|
|
135
|
+
return refuse(401, 'grant_invalid');
|
|
136
|
+
if (!claims.scopes.includes('channel:respond'))
|
|
137
|
+
return refuse(403, 'missing_channel_respond');
|
|
138
|
+
let body;
|
|
139
|
+
try {
|
|
140
|
+
body = (await req.json());
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return refuse(400, 'invalid_json');
|
|
144
|
+
}
|
|
145
|
+
// THE GRANT NAMES ONE CONVERSATION, and this is where that is enforced. The reference
|
|
146
|
+
// pack parsed this claim and never compared it, which made the binding decorative.
|
|
147
|
+
if (!claims.conversationId || claims.conversationId !== body.conversation?.id) {
|
|
148
|
+
return refuse(403, 'conversation_mismatch');
|
|
149
|
+
}
|
|
150
|
+
// A CONTRACT THIS SDK DOES NOT SPEAK IS A REFUSAL, NOT A GUESS. Absent means a core
|
|
151
|
+
// that predates the field. Anything else means core has changed the envelope's SHAPE,
|
|
152
|
+
// and answering it with v1 assumptions is how a pack confidently misreads a customer.
|
|
153
|
+
if (body.contract !== undefined && body.contract !== SUPPORTED_CONTRACT) {
|
|
154
|
+
return refuse(400, 'contract_unsupported');
|
|
155
|
+
}
|
|
156
|
+
// DEDUPED AFTER VERIFICATION, on ids the signature vouches for. Keying on the raw
|
|
157
|
+
// Idempotency-Key header would let an unauthenticated caller suppress a real delivery
|
|
158
|
+
// by guessing one, which is a denial of service dressed as an optimisation.
|
|
159
|
+
const key = `${claims.tenantId}:${body.conversation.id}:${body.delivery_id}`;
|
|
160
|
+
if (!seen.admit(key, Date.now()))
|
|
161
|
+
return accept('duplicate');
|
|
162
|
+
const work = think(claims, body, bearer).catch((cause) => {
|
|
163
|
+
// The delivery is forgotten so core's retry can be answered. The alternative — a
|
|
164
|
+
// remembered id and a thrown think — is a conversation that is never answered again
|
|
165
|
+
// and no record anywhere of why.
|
|
166
|
+
seen.forget(key);
|
|
167
|
+
console.error('[keemakr] channel turn failed', cause?.message);
|
|
168
|
+
});
|
|
169
|
+
// THE WORK RUNS AFTER THE RESPONSE, which is the whole reason this helper exists. A
|
|
170
|
+
// bare `void think()` after returning is what a first pack writes and what a serverless
|
|
171
|
+
// platform freezes at response end — every delivery then times out at core's answer
|
|
172
|
+
// deadline, and the symptom is "the agent is slow" rather than "the agent was killed".
|
|
173
|
+
if (waitUntil)
|
|
174
|
+
waitUntil(work);
|
|
175
|
+
else
|
|
176
|
+
void work;
|
|
177
|
+
return accept('accepted');
|
|
178
|
+
}
|
|
179
|
+
async function think(claims, body, bearer) {
|
|
180
|
+
const ctx = buildContext(claims, body, bearer);
|
|
181
|
+
let action;
|
|
182
|
+
try {
|
|
183
|
+
action = await options.answer(ctx);
|
|
184
|
+
}
|
|
185
|
+
catch (cause) {
|
|
186
|
+
// A PACK THAT THREW STILL OWES CORE AN ANSWER. Silence costs the customer the full
|
|
187
|
+
// answer deadline — ninety seconds of a chat window doing nothing — and then raises
|
|
188
|
+
// `answer_timed_out`, which reads as "the pack is hanging" rather than "the pack
|
|
189
|
+
// crashed". A handoff puts a person on it now and says which.
|
|
190
|
+
console.error('[keemakr] answer() threw', cause?.message);
|
|
191
|
+
action = {
|
|
192
|
+
action: 'handoff',
|
|
193
|
+
reason: 'agent_failed',
|
|
194
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
await postBack(claims, body, bearer, action);
|
|
198
|
+
}
|
|
199
|
+
function buildContext(claims, body, bearer) {
|
|
200
|
+
const authed = (path, init = {}) => fetchImpl(new URL(path, core.origin).toString(), {
|
|
201
|
+
...init,
|
|
202
|
+
headers: {
|
|
203
|
+
...init.headers,
|
|
204
|
+
authorization: `Bearer ${bearer}`,
|
|
205
|
+
'content-type': 'application/json',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
tenantId: claims.tenantId,
|
|
210
|
+
conversationId: body.conversation.id,
|
|
211
|
+
deliveryId: body.delivery_id,
|
|
212
|
+
messages: body.unanswered_messages ?? [],
|
|
213
|
+
truncated: body.truncated_messages ?? 0,
|
|
214
|
+
contact: body.contact,
|
|
215
|
+
binding: body.binding,
|
|
216
|
+
language: body.language,
|
|
217
|
+
async history(limit = 20) {
|
|
218
|
+
const res = await authed(`/api/channels/conversations/${encodeURIComponent(body.conversation.id)}?limit=${limit}`);
|
|
219
|
+
if (!res.ok)
|
|
220
|
+
return [];
|
|
221
|
+
const json = (await res.json());
|
|
222
|
+
return json.messages ?? [];
|
|
223
|
+
},
|
|
224
|
+
kb: {
|
|
225
|
+
async search(query, opts) {
|
|
226
|
+
const res = await authed('/api/capability/kb/retrieve', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
body: JSON.stringify({ query, limit: opts?.limit ?? 3 }),
|
|
229
|
+
});
|
|
230
|
+
if (!res.ok)
|
|
231
|
+
return [];
|
|
232
|
+
const json = (await res.json());
|
|
233
|
+
return (json.hits ?? []).map((h) => ({
|
|
234
|
+
chunkId: String(h.chunk_id ?? ''),
|
|
235
|
+
title: h.title ?? null,
|
|
236
|
+
text: String(h.text ?? ''),
|
|
237
|
+
score: Number(h.score ?? 0),
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
// PURE BUILDERS. They return an action and touch nothing — so a pack's `answer` can
|
|
242
|
+
// be tested by calling it and inspecting what came back, with no core, no fake and no
|
|
243
|
+
// network.
|
|
244
|
+
reply(text, opts) {
|
|
245
|
+
return {
|
|
246
|
+
action: 'reply',
|
|
247
|
+
text,
|
|
248
|
+
...(opts?.confidence !== undefined ? { confidence: opts.confidence } : {}),
|
|
249
|
+
...(opts?.groundedOn ? { grounded_on: opts.groundedOn } : {}),
|
|
250
|
+
usage: opts?.usage ?? { input_tokens: 0, output_tokens: 0 },
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
handoff(reason, opts) {
|
|
254
|
+
return {
|
|
255
|
+
action: 'handoff',
|
|
256
|
+
reason,
|
|
257
|
+
usage: opts?.usage ?? { input_tokens: 0, output_tokens: 0 },
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
none(opts) {
|
|
261
|
+
return { action: 'none', usage: opts?.usage ?? { input_tokens: 0, output_tokens: 0 } };
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
async function postBack(claims, body, bearer, action) {
|
|
266
|
+
const res = await fetchImpl(new URL(`/api/channels/conversations/${encodeURIComponent(body.conversation.id)}/actions`, core.origin).toString(), {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: {
|
|
269
|
+
authorization: `Bearer ${bearer}`,
|
|
270
|
+
'content-type': 'application/json',
|
|
271
|
+
'idempotency-key': body.delivery_id,
|
|
272
|
+
...(claims.traceId ? { 'x-keemakr-trace-id': claims.traceId } : {}),
|
|
273
|
+
},
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
delivery_id: body.delivery_id,
|
|
276
|
+
...action,
|
|
277
|
+
// ALWAYS, on every action including `none`. A pack that omits usage meters the
|
|
278
|
+
// whole lane at zero forever, and model tokens are most of what a conversation
|
|
279
|
+
// costs — so the one number that makes the bill visible is the one an author is
|
|
280
|
+
// most likely to forget.
|
|
281
|
+
usage: action.usage ?? { input_tokens: 0, output_tokens: 0 },
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) {
|
|
285
|
+
console.error(`[keemakr] core refused the answer: ${res.status}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function preview(req) {
|
|
289
|
+
const bearer = req.headers.get('authorization')?.replace(/^Bearer\s+/i, '') ?? '';
|
|
290
|
+
if (!bearer)
|
|
291
|
+
return refuse(401, 'no_grant');
|
|
292
|
+
const claims = await verifyChannelGrant(bearer, keys, options.audience);
|
|
293
|
+
if (!claims)
|
|
294
|
+
return refuse(401, 'grant_invalid');
|
|
295
|
+
// THE SAME SCOPE AS ANSWERING, deliberately. A preview reads the tenant's knowledge and
|
|
296
|
+
// runs their model budget; a weaker gate here would be a way to do both without being
|
|
297
|
+
// allowed to answer anybody.
|
|
298
|
+
if (!claims.scopes.includes('channel:respond'))
|
|
299
|
+
return refuse(403, 'missing_channel_respond');
|
|
300
|
+
let body;
|
|
301
|
+
try {
|
|
302
|
+
body = (await req.json());
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return refuse(400, 'invalid_json');
|
|
306
|
+
}
|
|
307
|
+
if (!claims.conversationId || claims.conversationId !== body.conversation?.id) {
|
|
308
|
+
return refuse(403, 'conversation_mismatch');
|
|
309
|
+
}
|
|
310
|
+
// AND THE SAME ENVELOPE CHECK AS A DELIVERY. This was in `handle` and not here, which
|
|
311
|
+
// is the one divergence that defeats the feature's purpose: a binding whose pack speaks
|
|
312
|
+
// a contract this SDK does not is correctly refused on the real path and silently
|
|
313
|
+
// ANSWERED on the preview path — so the owner is shown, under v1 assumptions, something
|
|
314
|
+
// no customer of theirs would ever receive. A preview that lies is worse than no
|
|
315
|
+
// preview, because it is trusted.
|
|
316
|
+
if (body.contract !== undefined && body.contract !== SUPPORTED_CONTRACT) {
|
|
317
|
+
return refuse(400, 'contract_unsupported');
|
|
318
|
+
}
|
|
319
|
+
const started = Date.now();
|
|
320
|
+
try {
|
|
321
|
+
const action = await options.answer(buildContext(claims, body, bearer));
|
|
322
|
+
return new Response(JSON.stringify({ action, latency_ms: Date.now() - started }), {
|
|
323
|
+
status: 200,
|
|
324
|
+
headers: { 'content-type': 'application/json' },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (cause) {
|
|
328
|
+
// SHOWN, not swallowed. The whole point of a preview is to find out why an agent is
|
|
329
|
+
// behaving oddly, and "it threw" is the most useful answer it can give.
|
|
330
|
+
return new Response(JSON.stringify({
|
|
331
|
+
action: {
|
|
332
|
+
action: 'handoff',
|
|
333
|
+
reason: 'agent_failed',
|
|
334
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
335
|
+
},
|
|
336
|
+
error: cause?.message ?? 'unknown',
|
|
337
|
+
latency_ms: Date.now() - started,
|
|
338
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
fetchHandler(req, ctx) {
|
|
343
|
+
return handle(req, ctx?.waitUntil ?? options.waitUntil);
|
|
344
|
+
},
|
|
345
|
+
previewHandler: preview,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function refuse(status, code) {
|
|
349
|
+
return new Response(JSON.stringify({ error: code }), {
|
|
350
|
+
status,
|
|
351
|
+
headers: { 'content-type': 'application/json' },
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function accept(state) {
|
|
355
|
+
return new Response(JSON.stringify({ accepted: true, state }), {
|
|
356
|
+
status: 202,
|
|
357
|
+
headers: { 'content-type': 'application/json' },
|
|
358
|
+
});
|
|
359
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { useKee, MemoryConflictError, RecordConflictError, RecordValidationError
|
|
|
4
4
|
export { keemakrToolDirectory } from './tool-directory.js';
|
|
5
5
|
export { refreshGrant, REFRESH_THRESHOLD_SECONDS } from './refresh.js';
|
|
6
6
|
export { imageCanvas, type Canvas, type ImageCanvas, type ReportCanvas } from './canvases.js';
|
|
7
|
+
export { createChannelAgent, assertTrustworthyCore, type ChannelAction, type ChannelAgent, type ChannelAgentOptions, type ChannelContext, type ChannelDelivery, type HandoffReason, type Usage, } from './channel.js';
|
package/dist/index.js
CHANGED
|
@@ -14,3 +14,4 @@ export { useKee, MemoryConflictError, RecordConflictError, RecordValidationError
|
|
|
14
14
|
export { keemakrToolDirectory } from './tool-directory.js';
|
|
15
15
|
export { refreshGrant, REFRESH_THRESHOLD_SECONDS } from './refresh.js';
|
|
16
16
|
export { imageCanvas } from './canvases.js';
|
|
17
|
+
export { createChannelAgent, assertTrustworthyCore, } from './channel.js';
|
package/dist/verify-grant.d.ts
CHANGED
|
@@ -7,15 +7,6 @@ export interface VerifiedGrant {
|
|
|
7
7
|
aud: string | null;
|
|
8
8
|
exp: number | null;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Verify a capability grant (session OR machine) against keemakr-core's JWKS.
|
|
12
|
-
* Returns the trusted claims, or `null` on any failure (bad signature, wrong
|
|
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.
|
|
18
|
-
*/
|
|
19
10
|
export declare function verifyGrant(token: string, opts?: {
|
|
20
11
|
jwksUrl?: string;
|
|
21
12
|
audience?: string;
|
package/dist/verify-grant.js
CHANGED
|
@@ -22,6 +22,16 @@ const GRANT_ISSUER = 'keemakr';
|
|
|
22
22
|
* default it did not choose. `audience` falls back to KEE_AGENT_AUDIENCE and is
|
|
23
23
|
* strongly recommended: a grant is only valid for the remote it was minted for.
|
|
24
24
|
*/
|
|
25
|
+
let audienceWarned = false;
|
|
26
|
+
/** Said once per process. A per-request warning is noise, and noise is not a warning. */
|
|
27
|
+
function warnAudienceUnset() {
|
|
28
|
+
if (audienceWarned)
|
|
29
|
+
return;
|
|
30
|
+
audienceWarned = true;
|
|
31
|
+
console.warn('[keemakr] KEE_AGENT_AUDIENCE is not set, so this grant is accepted whoever it was ' +
|
|
32
|
+
"minted for. Set it to this deployment's exact origin — the same URL registered as " +
|
|
33
|
+
"the entry's runtime — so a grant minted for another pack cannot be replayed here.");
|
|
34
|
+
}
|
|
25
35
|
export async function verifyGrant(token, opts) {
|
|
26
36
|
const jwksUrl = resolveJwksUrl(opts?.jwksUrl);
|
|
27
37
|
if (!jwksUrl) {
|
|
@@ -29,6 +39,19 @@ export async function verifyGrant(token, opts) {
|
|
|
29
39
|
return null;
|
|
30
40
|
}
|
|
31
41
|
const expectedAud = opts?.audience ?? process.env.KEE_AGENT_AUDIENCE;
|
|
42
|
+
// AN UNSET AUDIENCE IS A REAL RISK, SAID OUT LOUD ONCE.
|
|
43
|
+
//
|
|
44
|
+
// `jwtVerify` is given no `audience` option when this is empty, which means the grant is
|
|
45
|
+
// accepted whoever it was minted FOR — any core-signed token for any deployment of any
|
|
46
|
+
// pack passes. The behaviour is kept, because changing it would break every pack running
|
|
47
|
+
// without the variable today and a silent tightening is worse than a loud gap. But it was
|
|
48
|
+
// completely silent, which made the unsafe configuration the easy one: nothing at build
|
|
49
|
+
// time, nothing at boot, nothing in the log, and the symptom only appears if somebody
|
|
50
|
+
// points a different deployment at this one.
|
|
51
|
+
//
|
|
52
|
+
// Once per process, not per request: a warning on every turn is a warning nobody reads.
|
|
53
|
+
if (!expectedAud)
|
|
54
|
+
warnAudienceUnset();
|
|
32
55
|
try {
|
|
33
56
|
const { payload } = await jwtVerify(token, jwksFor(jwksUrl), {
|
|
34
57
|
issuer: GRANT_ISSUER,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keemakr/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0-dev-20260928074313",
|
|
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",
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
"./connectors": {
|
|
19
19
|
"types": "./dist/connectors.d.ts",
|
|
20
20
|
"import": "./dist/connectors.js"
|
|
21
|
+
},
|
|
22
|
+
"./channel": {
|
|
23
|
+
"types": "./dist/channel.d.ts",
|
|
24
|
+
"import": "./dist/channel.js"
|
|
25
|
+
},
|
|
26
|
+
"./channel/testkit": {
|
|
27
|
+
"types": "./dist/channel-testkit.d.ts",
|
|
28
|
+
"import": "./dist/channel-testkit.js"
|
|
21
29
|
}
|
|
22
30
|
},
|
|
23
31
|
"files": [
|
|
@@ -43,6 +51,11 @@
|
|
|
43
51
|
"eve": ">=0.40.0 <0.60",
|
|
44
52
|
"jose": "^6.2.3"
|
|
45
53
|
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"eve": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
46
59
|
"devDependencies": {
|
|
47
60
|
"@types/node": "^20.19.43",
|
|
48
61
|
"eve": "0.40.0",
|