blun-king-cli 9.1.469 → 9.1.470

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.
@@ -137,6 +137,24 @@ function publicErrorResponse(error) {
137
137
  },
138
138
  });
139
139
  }
140
+ if (code === 'AGENT_API_SESSION_NOT_FOUND') {
141
+ return jsonResponse(404, {
142
+ error: {
143
+ type: 'invalid_request_error',
144
+ code,
145
+ message: 'Agent session was not found.',
146
+ },
147
+ });
148
+ }
149
+ if (code === 'AGENT_API_SESSION_LIMIT' || code === 'AGENT_API_PRINCIPAL_LIMIT') {
150
+ return jsonResponse(429, {
151
+ error: {
152
+ type: 'rate_limit_error',
153
+ code,
154
+ message: 'Agent session limit reached.',
155
+ },
156
+ });
157
+ }
140
158
  if (code.startsWith('AGENT_API_INVALID_')) {
141
159
  return jsonResponse(400, {
142
160
  error: {
@@ -193,27 +211,35 @@ function createAgentApiHttpAdapter(options = {}) {
193
211
 
194
212
  function requireOwner(principalId, sessionId, requestId) {
195
213
  const key = scopeKey(sessionId, requestId);
196
- const owner = owners.get(key);
197
- if (owner === undefined) {
214
+ const scopeOwners = owners.get(key);
215
+ if (scopeOwners === undefined) {
198
216
  throw adapterError(
199
217
  'AGENT_API_TURN_NOT_ACTIVE',
200
218
  'Agent turn is not active.',
201
219
  409,
202
220
  );
203
221
  }
204
- if (owner !== principalId) {
222
+ const internalSessionId = scopeOwners.get(principalId);
223
+ if (internalSessionId === undefined) {
205
224
  throw adapterError(
206
225
  'AGENT_API_FORBIDDEN',
207
226
  'Agent turn belongs to a different account.',
208
227
  403,
209
228
  );
210
229
  }
211
- return key;
230
+ return { internalSessionId, key };
212
231
  }
213
232
 
214
- function trackScope(key, principalId, response) {
215
- if (responseNeedsToolResult(response)) owners.set(key, principalId);
216
- else owners.delete(key);
233
+ function trackScope(key, principalId, internalSessionId, response) {
234
+ if (responseNeedsToolResult(response)) {
235
+ const scopeOwners = owners.get(key) ?? new Map();
236
+ scopeOwners.set(principalId, internalSessionId);
237
+ owners.set(key, scopeOwners);
238
+ return;
239
+ }
240
+ const scopeOwners = owners.get(key);
241
+ scopeOwners?.delete(principalId);
242
+ if (scopeOwners?.size === 0) owners.delete(key);
217
243
  }
218
244
 
219
245
  async function handleStart(input, principalId) {
@@ -232,46 +258,53 @@ function createAgentApiHttpAdapter(options = {}) {
232
258
  sessionId,
233
259
  create: true,
234
260
  });
235
- if (session?.id !== sessionId) {
261
+ if (typeof session?.id !== 'string' || session.id.trim().length === 0) {
236
262
  throw adapterError(
237
263
  'AGENT_API_SESSION_SCOPE_MISMATCH',
238
- 'resolved session does not match session_id',
264
+ 'resolved session does not have an isolated session ID',
239
265
  403,
240
266
  );
241
267
  }
268
+ const internalSessionId = session.id.trim();
242
269
  const response = publicCompletion(await controller.startTurn({
243
270
  session,
271
+ publicSessionId: sessionId,
244
272
  requestId,
245
273
  prompt: normalizePrompt(body.input),
246
274
  tools: normalizeTools(body.tools),
247
275
  }));
248
- trackScope(scopeKey(sessionId, requestId), principalId, response);
276
+ trackScope(
277
+ scopeKey(sessionId, requestId),
278
+ principalId,
279
+ internalSessionId,
280
+ response,
281
+ );
249
282
  return jsonResponse(200, response);
250
283
  }
251
284
 
252
285
  async function handleContinue(input, principalId, requestId) {
253
286
  const body = requireBody(input.body);
254
287
  const sessionId = requirePublicId(body.session_id, 'session_id');
255
- const key = requireOwner(principalId, sessionId, requestId);
288
+ const owner = requireOwner(principalId, sessionId, requestId);
256
289
  const response = publicCompletion(await controller.continueTurn({
257
- sessionId,
290
+ sessionId: owner.internalSessionId,
258
291
  requestId,
259
292
  toolResults: body.tool_results,
260
293
  }));
261
- trackScope(key, principalId, response);
294
+ trackScope(owner.key, principalId, owner.internalSessionId, response);
262
295
  return jsonResponse(200, response);
263
296
  }
264
297
 
265
298
  async function handleCancel(input, principalId, requestId) {
266
299
  const body = requireBody(input.body ?? {});
267
300
  const sessionId = requirePublicId(body.session_id, 'session_id');
268
- const key = requireOwner(principalId, sessionId, requestId);
301
+ const owner = requireOwner(principalId, sessionId, requestId);
269
302
  await controller.cancelTurn({
270
- sessionId,
303
+ sessionId: owner.internalSessionId,
271
304
  requestId,
272
305
  reason: typeof body.reason === 'string' ? body.reason : undefined,
273
306
  });
274
- owners.delete(key);
307
+ trackScope(owner.key, principalId, owner.internalSessionId, null);
275
308
  return jsonResponse(200, {
276
309
  id: `agent-${requestId}`,
277
310
  object: 'agent.turn.cancelled',
@@ -0,0 +1,291 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const DEFAULT_MAX_SESSIONS_PER_PRINCIPAL = 16;
6
+ const DEFAULT_MAX_PRINCIPALS = 1024;
7
+ const PRINCIPAL_PATTERN = /^[A-Za-z0-9@._:+-]{1,256}$/u;
8
+ const PUBLIC_SESSION_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/u;
9
+
10
+ class AgentApiSessionRegistryError extends Error {
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.name = 'AgentApiSessionRegistryError';
14
+ this.code = code;
15
+ }
16
+ }
17
+
18
+ function registryError(code, message) {
19
+ return new AgentApiSessionRegistryError(code, message);
20
+ }
21
+
22
+ function requirePattern(value, pattern, label) {
23
+ if (typeof value !== 'string' || !pattern.test(value)) {
24
+ throw registryError('AGENT_API_INVALID_SCOPE', `${label} is invalid`);
25
+ }
26
+ return value;
27
+ }
28
+
29
+ function requireRootDir(value) {
30
+ if (typeof value !== 'string' || value.trim().length === 0) {
31
+ throw new TypeError('rootDir must be a non-empty path');
32
+ }
33
+ return path.resolve(value);
34
+ }
35
+
36
+ function requireLimit(value) {
37
+ if (!Number.isSafeInteger(value) || value < 1) {
38
+ throw new TypeError('maxSessionsPerPrincipal must be a positive integer');
39
+ }
40
+ return value;
41
+ }
42
+
43
+ function validateIsolationSecret(value) {
44
+ if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') < 32) {
45
+ throw new TypeError('isolationSecret must contain at least 32 bytes');
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function opaqueHash(secret, ...parts) {
51
+ return createHmac('sha256', secret).update(parts.join('\0')).digest('hex');
52
+ }
53
+
54
+ function validateHarness(harness) {
55
+ if (harness === null || typeof harness !== 'object'
56
+ || typeof harness.createSession !== 'function') {
57
+ throw new TypeError('createHarness must return a harness with createSession');
58
+ }
59
+ return harness;
60
+ }
61
+
62
+ function validateSession(session, internalSessionId) {
63
+ if (session === null || typeof session !== 'object' || session.id !== internalSessionId) {
64
+ throw registryError(
65
+ 'AGENT_API_SESSION_SCOPE_MISMATCH',
66
+ 'SDK session does not match the isolated session scope',
67
+ );
68
+ }
69
+ return session;
70
+ }
71
+
72
+ function normalizeSessionOptions(value) {
73
+ if (value === undefined) return {};
74
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
75
+ throw new TypeError('sessionOptions must resolve to an object');
76
+ }
77
+ return { ...value };
78
+ }
79
+
80
+ function createAgentApiSessionRegistry(options = {}) {
81
+ const rootDir = requireRootDir(options.rootDir);
82
+ const isolationSecret = validateIsolationSecret(options.isolationSecret);
83
+ if (typeof options.createHarness !== 'function') {
84
+ throw new TypeError('createHarness must be a function');
85
+ }
86
+ if (options.prepareHarness !== undefined && typeof options.prepareHarness !== 'function') {
87
+ throw new TypeError('prepareHarness must be a function');
88
+ }
89
+ if (options.sessionOptions !== undefined
90
+ && typeof options.sessionOptions !== 'function'
91
+ && (options.sessionOptions === null
92
+ || typeof options.sessionOptions !== 'object'
93
+ || Array.isArray(options.sessionOptions))) {
94
+ throw new TypeError('sessionOptions must be an object or function');
95
+ }
96
+
97
+ const maxSessionsPerPrincipal = requireLimit(
98
+ options.maxSessionsPerPrincipal ?? DEFAULT_MAX_SESSIONS_PER_PRINCIPAL,
99
+ );
100
+ const maxPrincipals = requireLimit(options.maxPrincipals ?? DEFAULT_MAX_PRINCIPALS);
101
+ const principals = new Map();
102
+ let closed = false;
103
+
104
+ function ensureOpen() {
105
+ if (closed) {
106
+ throw registryError('AGENT_API_SESSION_REGISTRY_CLOSED', 'session registry is closed');
107
+ }
108
+ }
109
+
110
+ function createPrincipalRecord(principalId) {
111
+ const principalKey = opaqueHash(isolationSecret, 'principal', principalId).slice(0, 40);
112
+ const principalRoot = path.join(rootDir, 'principals', principalKey);
113
+ const homeDir = path.join(principalRoot, 'home');
114
+ const workRoot = path.join(principalRoot, 'work');
115
+ const record = {
116
+ harnessReady: false,
117
+ harnessPromise: undefined,
118
+ homeDir,
119
+ principalKey,
120
+ principalRoot,
121
+ sessions: new Map(),
122
+ workRoot,
123
+ };
124
+ record.harnessPromise = (async () => {
125
+ await mkdir(homeDir, { recursive: true });
126
+ await mkdir(workRoot, { recursive: true });
127
+ const harness = validateHarness(await options.createHarness({
128
+ homeDir,
129
+ principalKey,
130
+ uiMode: 'api',
131
+ }));
132
+ try {
133
+ if (typeof harness.ensureConfigFile === 'function') {
134
+ await harness.ensureConfigFile();
135
+ }
136
+ if (options.prepareHarness !== undefined) {
137
+ await options.prepareHarness({ harness, homeDir, principalKey });
138
+ }
139
+ record.harnessReady = true;
140
+ } catch (error) {
141
+ try {
142
+ await harness.close?.();
143
+ } catch {
144
+ // Preserve the preparation error.
145
+ }
146
+ throw error;
147
+ }
148
+ return harness;
149
+ })();
150
+ principals.set(principalId, record);
151
+ return record;
152
+ }
153
+
154
+ function getOrCreatePrincipal(principalId) {
155
+ const existing = principals.get(principalId);
156
+ if (existing !== undefined) return existing;
157
+ if (principals.size >= maxPrincipals) {
158
+ throw registryError(
159
+ 'AGENT_API_PRINCIPAL_LIMIT',
160
+ 'Agent API active customer limit reached',
161
+ );
162
+ }
163
+ return createPrincipalRecord(principalId);
164
+ }
165
+
166
+ async function sessionOptionsFor(input) {
167
+ const configured = typeof options.sessionOptions === 'function'
168
+ ? await options.sessionOptions(input)
169
+ : options.sessionOptions;
170
+ return normalizeSessionOptions(configured);
171
+ }
172
+
173
+ async function createSession(record, principalId, publicSessionId) {
174
+ const sessionHash = opaqueHash(
175
+ isolationSecret,
176
+ 'session',
177
+ principalId,
178
+ publicSessionId,
179
+ );
180
+ const internalSessionId = `agent_api_${sessionHash.slice(0, 48)}`;
181
+ const workDir = path.join(record.workRoot, sessionHash.slice(0, 48));
182
+ await mkdir(workDir, { recursive: true });
183
+ const harness = await record.harnessPromise;
184
+ const configured = await sessionOptionsFor({
185
+ internalSessionId,
186
+ principalKey: record.principalKey,
187
+ publicSessionId,
188
+ workDir,
189
+ });
190
+ const session = await harness.createSession({
191
+ ...configured,
192
+ id: internalSessionId,
193
+ model: configured.model ?? 'king',
194
+ permission: configured.permission ?? 'auto',
195
+ workDir,
196
+ });
197
+ return validateSession(session, internalSessionId);
198
+ }
199
+
200
+ async function resolveSession(input) {
201
+ ensureOpen();
202
+ if (input === null || typeof input !== 'object') {
203
+ throw registryError('AGENT_API_INVALID_SCOPE', 'session scope is required');
204
+ }
205
+ const principalId = requirePattern(input.principalId, PRINCIPAL_PATTERN, 'principalId');
206
+ const publicSessionId = requirePattern(
207
+ input.sessionId,
208
+ PUBLIC_SESSION_PATTERN,
209
+ 'sessionId',
210
+ );
211
+ let record = principals.get(principalId);
212
+ const existing = record?.sessions.get(publicSessionId);
213
+ if (existing !== undefined) return existing;
214
+ if (input.create !== true) {
215
+ throw registryError('AGENT_API_SESSION_NOT_FOUND', 'Agent API session was not found');
216
+ }
217
+ record = getOrCreatePrincipal(principalId);
218
+ if (record.sessions.size >= maxSessionsPerPrincipal) {
219
+ throw registryError(
220
+ 'AGENT_API_SESSION_LIMIT',
221
+ 'Agent API session limit reached for this customer',
222
+ );
223
+ }
224
+
225
+ const pending = createSession(record, principalId, publicSessionId);
226
+ record.sessions.set(publicSessionId, pending);
227
+ try {
228
+ return await pending;
229
+ } catch (error) {
230
+ if (record.sessions.get(publicSessionId) === pending) {
231
+ record.sessions.delete(publicSessionId);
232
+ }
233
+ if (!record.harnessReady
234
+ && record.sessions.size === 0
235
+ && principals.get(principalId) === record) {
236
+ principals.delete(principalId);
237
+ }
238
+ throw error;
239
+ }
240
+ }
241
+
242
+ async function closeSession(input) {
243
+ ensureOpen();
244
+ if (input === null || typeof input !== 'object') {
245
+ throw registryError('AGENT_API_INVALID_SCOPE', 'session scope is required');
246
+ }
247
+ const principalId = requirePattern(input.principalId, PRINCIPAL_PATTERN, 'principalId');
248
+ const publicSessionId = requirePattern(
249
+ input.sessionId,
250
+ PUBLIC_SESSION_PATTERN,
251
+ 'sessionId',
252
+ );
253
+ const record = principals.get(principalId);
254
+ const pending = record?.sessions.get(publicSessionId);
255
+ if (pending === undefined) return false;
256
+ record.sessions.delete(publicSessionId);
257
+ const session = await pending;
258
+ await session.close?.();
259
+ if (record.sessions.size === 0 && principals.get(principalId) === record) {
260
+ principals.delete(principalId);
261
+ const harness = await record.harnessPromise.catch(() => null);
262
+ await harness?.close?.();
263
+ }
264
+ return true;
265
+ }
266
+
267
+ async function close() {
268
+ if (closed) return;
269
+ closed = true;
270
+ const records = [...principals.values()];
271
+ principals.clear();
272
+ for (const record of records) {
273
+ const sessions = await Promise.allSettled([...record.sessions.values()]);
274
+ record.sessions.clear();
275
+ for (const outcome of sessions) {
276
+ if (outcome.status === 'fulfilled') await outcome.value.close?.();
277
+ }
278
+ const harness = await record.harnessPromise.catch(() => null);
279
+ await harness?.close?.();
280
+ }
281
+ }
282
+
283
+ return Object.freeze({ close, closeSession, resolveSession });
284
+ }
285
+
286
+ export {
287
+ AgentApiSessionRegistryError,
288
+ DEFAULT_MAX_PRINCIPALS,
289
+ DEFAULT_MAX_SESSIONS_PER_PRINCIPAL,
290
+ createAgentApiSessionRegistry,
291
+ };
@@ -123,7 +123,7 @@ function openAiEnvelope(turn, message, finishReason) {
123
123
  blun: {
124
124
  mode: 'agent',
125
125
  task_id: turn.taskId,
126
- session_id: turn.sessionId,
126
+ session_id: turn.publicSessionId,
127
127
  request_id: turn.requestId,
128
128
  },
129
129
  };
@@ -337,6 +337,9 @@ function createAgentApiTurnController(options = {}) {
337
337
  throw controllerError('AGENT_API_INVALID_REQUEST', 'turn input must be an object');
338
338
  }
339
339
  const sessionId = validateSession(input.session);
340
+ const publicSessionId = input.publicSessionId === undefined
341
+ ? sessionId
342
+ : requireId(input.publicSessionId, 'publicSessionId');
340
343
  const requestId = requireId(input.requestId, 'requestId');
341
344
  if (activeTurns.has(sessionId)) {
342
345
  throw controllerError(
@@ -360,6 +363,7 @@ function createAgentApiTurnController(options = {}) {
360
363
  closed: false,
361
364
  created: Math.floor(Number(now()) / 1000),
362
365
  publicModel,
366
+ publicSessionId,
363
367
  requestId,
364
368
  session: input.session,
365
369
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.469",
3
+ "version": "9.1.470",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {