blun-king-cli 9.1.468 → 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
|
|
197
|
-
if (
|
|
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
|
-
|
|
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))
|
|
216
|
-
|
|
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 !==
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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,268 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
5
|
+
const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
6
|
+
const INTERNAL_SECRET_HEADER = 'x-blun-agent-secret';
|
|
7
|
+
const PRINCIPAL_HEADER = 'x-blun-principal-id';
|
|
8
|
+
const PRINCIPAL_PATTERN = /^[A-Za-z0-9._:-]{1,256}$/u;
|
|
9
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1']);
|
|
10
|
+
|
|
11
|
+
class AgentApiPrivateHttpServerError extends Error {
|
|
12
|
+
constructor(code, message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'AgentApiPrivateHttpServerError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.status = status;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function serverError(code, message, status) {
|
|
21
|
+
return new AgentApiPrivateHttpServerError(code, message, status);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateAdapter(adapter) {
|
|
25
|
+
if (adapter === null || typeof adapter !== 'object'
|
|
26
|
+
|| typeof adapter.handleRequest !== 'function') {
|
|
27
|
+
throw new TypeError('adapter.handleRequest is required');
|
|
28
|
+
}
|
|
29
|
+
return adapter;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateSecret(secret) {
|
|
33
|
+
if (typeof secret !== 'string' || Buffer.byteLength(secret, 'utf8') < 32) {
|
|
34
|
+
throw new TypeError('internalSecret must contain at least 32 bytes');
|
|
35
|
+
}
|
|
36
|
+
return secret;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateMaxBodyBytes(value) {
|
|
40
|
+
if (!Number.isSafeInteger(value) || value < 1024) {
|
|
41
|
+
throw new TypeError('maxBodyBytes must be an integer of at least 1024');
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function singleHeader(headers, name) {
|
|
47
|
+
const value = headers[name];
|
|
48
|
+
return typeof value === 'string' ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function secretMatches(actual, expected) {
|
|
52
|
+
if (typeof actual !== 'string') return false;
|
|
53
|
+
const actualHash = createHash('sha256').update(actual, 'utf8').digest();
|
|
54
|
+
const expectedHash = createHash('sha256').update(expected, 'utf8').digest();
|
|
55
|
+
return timingSafeEqual(actualHash, expectedHash);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requirePrincipal(headers) {
|
|
59
|
+
const principalId = singleHeader(headers, PRINCIPAL_HEADER);
|
|
60
|
+
if (principalId === undefined || !PRINCIPAL_PATTERN.test(principalId)) {
|
|
61
|
+
throw serverError(
|
|
62
|
+
'AGENT_API_UNAUTHENTICATED',
|
|
63
|
+
'Trusted Agent API principal is required.',
|
|
64
|
+
401,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return principalId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function declaredLength(headers) {
|
|
71
|
+
const raw = singleHeader(headers, 'content-length');
|
|
72
|
+
if (raw === undefined) return undefined;
|
|
73
|
+
if (!/^\d+$/u.test(raw)) {
|
|
74
|
+
throw serverError('AGENT_API_INVALID_BODY', 'Request body length is invalid.', 400);
|
|
75
|
+
}
|
|
76
|
+
const value = Number(raw);
|
|
77
|
+
if (!Number.isSafeInteger(value)) {
|
|
78
|
+
throw serverError('AGENT_API_INVALID_BODY', 'Request body length is invalid.', 400);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function collectBody(request, maxBodyBytes) {
|
|
84
|
+
const length = declaredLength(request.headers);
|
|
85
|
+
if (length !== undefined && length > maxBodyBytes) {
|
|
86
|
+
request.resume();
|
|
87
|
+
throw serverError('AGENT_API_BODY_TOO_LARGE', 'Agent API request is too large.', 413);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const chunks = [];
|
|
92
|
+
let size = 0;
|
|
93
|
+
let settled = false;
|
|
94
|
+
|
|
95
|
+
request.on('data', (chunk) => {
|
|
96
|
+
if (settled) return;
|
|
97
|
+
size += chunk.length;
|
|
98
|
+
if (size > maxBodyBytes) {
|
|
99
|
+
settled = true;
|
|
100
|
+
chunks.length = 0;
|
|
101
|
+
request.resume();
|
|
102
|
+
reject(serverError(
|
|
103
|
+
'AGENT_API_BODY_TOO_LARGE',
|
|
104
|
+
'Agent API request is too large.',
|
|
105
|
+
413,
|
|
106
|
+
));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
chunks.push(chunk);
|
|
110
|
+
});
|
|
111
|
+
request.on('end', () => {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
settled = true;
|
|
114
|
+
resolve(Buffer.concat(chunks, size));
|
|
115
|
+
});
|
|
116
|
+
request.on('aborted', () => {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
settled = true;
|
|
119
|
+
reject(serverError('AGENT_API_INVALID_BODY', 'Agent API request was aborted.', 400));
|
|
120
|
+
});
|
|
121
|
+
request.on('error', (error) => {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
settled = true;
|
|
124
|
+
reject(error);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readJsonBody(request, maxBodyBytes) {
|
|
130
|
+
const buffer = await collectBody(request, maxBodyBytes);
|
|
131
|
+
if (buffer.length === 0) return {};
|
|
132
|
+
const contentType = singleHeader(request.headers, 'content-type');
|
|
133
|
+
if (contentType === undefined || !/^application\/json(?:\s*;|\s*$)/iu.test(contentType)) {
|
|
134
|
+
throw serverError(
|
|
135
|
+
'AGENT_API_UNSUPPORTED_MEDIA_TYPE',
|
|
136
|
+
'Agent API requests must use application/json.',
|
|
137
|
+
415,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(buffer.toString('utf8'));
|
|
142
|
+
} catch {
|
|
143
|
+
throw serverError('AGENT_API_INVALID_JSON', 'Request body is not valid JSON.', 400);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function publicError(error) {
|
|
148
|
+
if (error instanceof AgentApiPrivateHttpServerError) {
|
|
149
|
+
return {
|
|
150
|
+
status: error.status,
|
|
151
|
+
body: {
|
|
152
|
+
error: {
|
|
153
|
+
type: 'invalid_request_error',
|
|
154
|
+
code: error.code,
|
|
155
|
+
message: error.message,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
status: 500,
|
|
162
|
+
body: {
|
|
163
|
+
error: {
|
|
164
|
+
type: 'internal_error',
|
|
165
|
+
code: 'AGENT_API_INTERNAL',
|
|
166
|
+
message: 'Agent request failed.',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function writeJson(response, status, body, headers = {}) {
|
|
173
|
+
if (response.headersSent || response.destroyed) return;
|
|
174
|
+
response.writeHead(status, {
|
|
175
|
+
...headers,
|
|
176
|
+
'cache-control': 'no-store',
|
|
177
|
+
'content-type': 'application/json',
|
|
178
|
+
'x-content-type-options': 'nosniff',
|
|
179
|
+
});
|
|
180
|
+
response.end(JSON.stringify(body));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createAgentApiPrivateHttpServer(options = {}) {
|
|
184
|
+
const adapter = validateAdapter(options.adapter);
|
|
185
|
+
const internalSecret = validateSecret(options.internalSecret);
|
|
186
|
+
const maxBodyBytes = validateMaxBodyBytes(
|
|
187
|
+
options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
async function handle(request, response) {
|
|
191
|
+
try {
|
|
192
|
+
if (!secretMatches(singleHeader(request.headers, INTERNAL_SECRET_HEADER), internalSecret)) {
|
|
193
|
+
request.resume();
|
|
194
|
+
throw serverError('AGENT_API_UNAUTHENTICATED', 'Agent API authentication failed.', 401);
|
|
195
|
+
}
|
|
196
|
+
const principalId = requirePrincipal(request.headers);
|
|
197
|
+
const body = await readJsonBody(request, maxBodyBytes);
|
|
198
|
+
const result = await adapter.handleRequest({
|
|
199
|
+
method: request.method,
|
|
200
|
+
path: request.url,
|
|
201
|
+
principalId,
|
|
202
|
+
body,
|
|
203
|
+
});
|
|
204
|
+
if (result === null) {
|
|
205
|
+
writeJson(response, 404, {
|
|
206
|
+
error: {
|
|
207
|
+
type: 'invalid_request_error',
|
|
208
|
+
code: 'AGENT_API_ROUTE_NOT_FOUND',
|
|
209
|
+
message: 'Agent API route not found.',
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
writeJson(response, result.status, result.body, result.headers);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const result = publicError(error);
|
|
217
|
+
writeJson(response, result.status, result.body);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const server = createServer((request, response) => {
|
|
222
|
+
void handle(request, response);
|
|
223
|
+
});
|
|
224
|
+
server.maxHeadersCount = 64;
|
|
225
|
+
server.headersTimeout = 10_000;
|
|
226
|
+
server.requestTimeout = 30_000;
|
|
227
|
+
server.keepAliveTimeout = 5_000;
|
|
228
|
+
|
|
229
|
+
function listen(input = {}) {
|
|
230
|
+
const host = input.host ?? DEFAULT_HOST;
|
|
231
|
+
const port = input.port ?? 0;
|
|
232
|
+
if (!LOOPBACK_HOSTS.has(host)) {
|
|
233
|
+
throw new TypeError('Agent API private server may listen on loopback only');
|
|
234
|
+
}
|
|
235
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
236
|
+
throw new TypeError('port must be an integer from 0 through 65535');
|
|
237
|
+
}
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const onError = (error) => reject(error);
|
|
240
|
+
server.once('error', onError);
|
|
241
|
+
server.listen({ host, port, exclusive: true }, () => {
|
|
242
|
+
server.off('error', onError);
|
|
243
|
+
resolve(server.address());
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function close() {
|
|
249
|
+
if (!server.listening) return Promise.resolve();
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
server.close((error) => {
|
|
252
|
+
if (error) reject(error);
|
|
253
|
+
else resolve();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return Object.freeze({ close, listen, server });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export {
|
|
262
|
+
AgentApiPrivateHttpServerError,
|
|
263
|
+
DEFAULT_HOST,
|
|
264
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
265
|
+
INTERNAL_SECRET_HEADER,
|
|
266
|
+
PRINCIPAL_HEADER,
|
|
267
|
+
createAgentApiPrivateHttpServer,
|
|
268
|
+
};
|
|
@@ -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.
|
|
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,
|