blun-king-cli 9.1.467 → 9.1.469
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/bin/agent-api-private-http-server.mjs +268 -0
- package/bin/agent-api-runtime.mjs +11 -11
- package/bin/subagent-tool-policy.cjs +60 -0
- package/blun.mjs +4 -1
- package/package.json +1 -1
|
@@ -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
|
+
};
|
|
@@ -6,7 +6,12 @@ const { createAgentApiToolBroker } = require('./agent-api-tool-broker.cjs');
|
|
|
6
6
|
|
|
7
7
|
const CLIENT_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/u;
|
|
8
8
|
const INTERNAL_TOOL_PREFIX = 'client__';
|
|
9
|
-
const
|
|
9
|
+
const AGENT_API_INTERNAL_TOOLS = Object.freeze([
|
|
10
|
+
'Agent',
|
|
11
|
+
'TaskList',
|
|
12
|
+
'TaskOutput',
|
|
13
|
+
'TaskStop',
|
|
14
|
+
]);
|
|
10
15
|
|
|
11
16
|
class AgentApiRuntimeError extends Error {
|
|
12
17
|
constructor(code, message) {
|
|
@@ -112,10 +117,6 @@ function internalToolName(publicName, index) {
|
|
|
112
117
|
return `${prefix}${publicName.slice(0, available)}${suffix}`;
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
function noClientToolsSentinel(sessionId, requestId) {
|
|
116
|
-
return `${NO_CLIENT_TOOLS_PREFIX}${shortHash(`${sessionId}\0${requestId}`, 16)}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
120
|
function validateSession(session) {
|
|
120
121
|
if (session === null || typeof session !== 'object') {
|
|
121
122
|
throw runtimeError('AGENT_API_INVALID_SESSION', 'session must be an SDK session');
|
|
@@ -188,11 +189,10 @@ function createAgentApiRuntime(options = {}) {
|
|
|
188
189
|
registeredNames.push(tool.internalName);
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
await input.session.setAllowedTools(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
);
|
|
192
|
+
await input.session.setAllowedTools([
|
|
193
|
+
...AGENT_API_INTERNAL_TOOLS,
|
|
194
|
+
...registeredNames,
|
|
195
|
+
]);
|
|
196
196
|
} catch (error) {
|
|
197
197
|
broker.abortRequest({
|
|
198
198
|
sessionId,
|
|
@@ -244,9 +244,9 @@ function createAgentApiRuntime(options = {}) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
export {
|
|
247
|
+
AGENT_API_INTERNAL_TOOLS,
|
|
247
248
|
AgentApiRuntimeError,
|
|
248
249
|
CLIENT_TOOL_NAME_PATTERN,
|
|
249
|
-
NO_CLIENT_TOOLS_PREFIX,
|
|
250
250
|
createAgentApiRuntime,
|
|
251
251
|
normalizeClientToolDefinitions,
|
|
252
252
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const NESTED_AGENT_LIFECYCLE_TOOLS = Object.freeze([
|
|
4
|
+
'Agent',
|
|
5
|
+
'TaskList',
|
|
6
|
+
'TaskOutput',
|
|
7
|
+
'TaskStop',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function validateParentAllowedTools(parentTools) {
|
|
11
|
+
if (parentTools === null || typeof parentTools !== 'object') {
|
|
12
|
+
throw new TypeError('parentTools is required');
|
|
13
|
+
}
|
|
14
|
+
const allowed = parentTools.allowedTools;
|
|
15
|
+
if (allowed !== undefined && !Array.isArray(allowed)) {
|
|
16
|
+
throw new TypeError('parentTools.allowedTools must be an array or undefined');
|
|
17
|
+
}
|
|
18
|
+
return allowed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function inheritSubagentLifecycleTools(parentTools, childTools) {
|
|
22
|
+
const allowed = validateParentAllowedTools(parentTools);
|
|
23
|
+
if (allowed === undefined) return [];
|
|
24
|
+
if (childTools === null || typeof childTools !== 'object'
|
|
25
|
+
|| typeof childTools.data !== 'function'
|
|
26
|
+
|| typeof childTools.setActiveTools !== 'function'
|
|
27
|
+
|| typeof childTools.refreshBuiltinTools !== 'function') {
|
|
28
|
+
throw new TypeError('childTools lifecycle controls are required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const toolData = childTools.data();
|
|
32
|
+
if (!Array.isArray(toolData)) throw new TypeError('childTools.data() must return an array');
|
|
33
|
+
const available = new Set(toolData.map((tool) => tool?.name).filter(Boolean));
|
|
34
|
+
const inherited = NESTED_AGENT_LIFECYCLE_TOOLS.filter((name) => (
|
|
35
|
+
allowed.includes(name) && available.has(name)
|
|
36
|
+
));
|
|
37
|
+
if (inherited.length === 0) return [];
|
|
38
|
+
|
|
39
|
+
const active = toolData.filter((tool) => tool?.active === true).map((tool) => tool.name);
|
|
40
|
+
childTools.setActiveTools([...new Set([...active, ...inherited])]);
|
|
41
|
+
childTools.refreshBuiltinTools();
|
|
42
|
+
return inherited;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function inheritSubagentAllowedTools(parentTools, childTools) {
|
|
46
|
+
const allowed = validateParentAllowedTools(parentTools);
|
|
47
|
+
if (childTools === null || typeof childTools !== 'object'
|
|
48
|
+
|| typeof childTools.setAllowedTools !== 'function') {
|
|
49
|
+
throw new TypeError('childTools.setAllowedTools is required');
|
|
50
|
+
}
|
|
51
|
+
const inherited = allowed === undefined ? undefined : [...allowed];
|
|
52
|
+
childTools.setAllowedTools(inherited);
|
|
53
|
+
return inherited;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
NESTED_AGENT_LIFECYCLE_TOOLS,
|
|
58
|
+
inheritSubagentAllowedTools,
|
|
59
|
+
inheritSubagentLifecycleTools,
|
|
60
|
+
};
|
package/blun.mjs
CHANGED
|
@@ -252406,7 +252406,7 @@ function shouldSuppressQueuedAttemptFailureEvent(options, error) {
|
|
|
252406
252406
|
if (isProviderRateLimitError(error)) return true;
|
|
252407
252407
|
return isAbortError$4(error) || options.signal.aborted;
|
|
252408
252408
|
}
|
|
252409
|
-
var DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation, buildSubagentUsageDelta, SUBAGENT_MAX_TOKENS_ERROR, SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT, buildSubagentMaxTokensFailure, shouldRequestSubagentHandoff, SUMMARY_MIN_LENGTH, SUMMARY_CONTINUATION_ATTEMPTS, HOOK_TEXT_PREVIEW_LENGTH, TOOL_CALL_DISABLED_MESSAGE, SUBAGENT_PROMPT_ORIGIN, SIDE_QUESTION_SYSTEM_REMINDER, SessionSubagentHost;
|
|
252409
|
+
var DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation, buildSubagentUsageDelta, inheritSubagentAllowedTools, inheritSubagentLifecycleTools, SUBAGENT_MAX_TOKENS_ERROR, SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT, buildSubagentMaxTokensFailure, shouldRequestSubagentHandoff, SUMMARY_MIN_LENGTH, SUMMARY_CONTINUATION_ATTEMPTS, HOOK_TEXT_PREVIEW_LENGTH, TOOL_CALL_DISABLED_MESSAGE, SUBAGENT_PROMPT_ORIGIN, SIDE_QUESTION_SYSTEM_REMINDER, SessionSubagentHost;
|
|
252410
252410
|
var init_subagent_host = __esmMin((() => {
|
|
252411
252411
|
init_src$4();
|
|
252412
252412
|
init_errors$8();
|
|
@@ -252420,6 +252420,7 @@ var init_subagent_host = __esmMin((() => {
|
|
|
252420
252420
|
init_summary_continuation();
|
|
252421
252421
|
({ DEFAULT_SUBAGENT_TIMEOUT_MS, resolveSubagentTimeoutMs, runAgentWithTimeoutContinuation } = createRequire(import.meta.url)("./bin/subagent-timeout-policy.cjs"));
|
|
252422
252422
|
({ buildSubagentUsageDelta } = createRequire(import.meta.url)("./bin/subagent-usage-rollup-policy.cjs"));
|
|
252423
|
+
({ inheritSubagentAllowedTools, inheritSubagentLifecycleTools } = createRequire(import.meta.url)("./bin/subagent-tool-policy.cjs"));
|
|
252423
252424
|
({
|
|
252424
252425
|
SUBAGENT_MAX_TOKENS_ERROR,
|
|
252425
252426
|
SUBAGENT_MAX_TOKENS_HANDOFF_PROMPT,
|
|
@@ -252729,6 +252730,8 @@ IMPORTANT:
|
|
|
252729
252730
|
const context = await prepareSystemPromptContext(this.session.systemContextKaos(child.kaos.getcwd()), this.session.options.blunHomeDir, { additionalDirs: child.getAdditionalDirs() });
|
|
252730
252731
|
child.useProfile(profile, context, this.session.options.blunHomeDir);
|
|
252731
252732
|
child.tools.inheritUserTools(parent.tools);
|
|
252733
|
+
inheritSubagentLifecycleTools(parent.tools, child.tools);
|
|
252734
|
+
inheritSubagentAllowedTools(parent.tools, child.tools);
|
|
252732
252735
|
}
|
|
252733
252736
|
async triggerSubagentStart(parent, profileName, prompt, signal) {
|
|
252734
252737
|
await parent.hooks?.trigger("SubagentStart", {
|