@harapter/transport-acp 0.1.1
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/LICENSE +201 -0
- package/README.md +184 -0
- package/dist/client.d.ts +81 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +644 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +395 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +47 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +1181 -0
- package/dist/validation.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { acpError } from './errors.js';
|
|
4
|
+
import { ACP_PROTOCOL_VERSION, } from './types.js';
|
|
5
|
+
const maximumStringLength = 1024 * 1024;
|
|
6
|
+
const maximumIdentifierLength = 1024;
|
|
7
|
+
const maximumRawDepth = 4;
|
|
8
|
+
const maximumRawNodes = 64;
|
|
9
|
+
const maximumRawItems = 16;
|
|
10
|
+
const knownMethods = new Set(['session/update']);
|
|
11
|
+
const structuralStringKeys = new Set([
|
|
12
|
+
'kind',
|
|
13
|
+
'outcome',
|
|
14
|
+
'sessionUpdate',
|
|
15
|
+
'status',
|
|
16
|
+
'type',
|
|
17
|
+
]);
|
|
18
|
+
const knownStructuralValues = new Set([
|
|
19
|
+
'agent_message_chunk',
|
|
20
|
+
'agent_thought_chunk',
|
|
21
|
+
'available_commands_update',
|
|
22
|
+
'cancelled',
|
|
23
|
+
'completed',
|
|
24
|
+
'config_option_update',
|
|
25
|
+
'content',
|
|
26
|
+
'diff',
|
|
27
|
+
'failed',
|
|
28
|
+
'in_progress',
|
|
29
|
+
'pending',
|
|
30
|
+
'plan',
|
|
31
|
+
'session_info_update',
|
|
32
|
+
'terminal',
|
|
33
|
+
'text',
|
|
34
|
+
'tool_call',
|
|
35
|
+
'tool_call_update',
|
|
36
|
+
'usage_update',
|
|
37
|
+
'user_message_chunk',
|
|
38
|
+
]);
|
|
39
|
+
const toolKinds = new Set([
|
|
40
|
+
'read',
|
|
41
|
+
'edit',
|
|
42
|
+
'delete',
|
|
43
|
+
'move',
|
|
44
|
+
'search',
|
|
45
|
+
'execute',
|
|
46
|
+
'think',
|
|
47
|
+
'fetch',
|
|
48
|
+
'switch_mode',
|
|
49
|
+
'other',
|
|
50
|
+
]);
|
|
51
|
+
const toolStatuses = new Set([
|
|
52
|
+
'pending',
|
|
53
|
+
'in_progress',
|
|
54
|
+
'completed',
|
|
55
|
+
'failed',
|
|
56
|
+
]);
|
|
57
|
+
const stopReasons = new Set([
|
|
58
|
+
'end_turn',
|
|
59
|
+
'max_tokens',
|
|
60
|
+
'max_turn_requests',
|
|
61
|
+
'refusal',
|
|
62
|
+
'cancelled',
|
|
63
|
+
]);
|
|
64
|
+
/** Validate and normalize the stable initialize payload sent to the Agent. */
|
|
65
|
+
export function prepareInitializeInput(input = {}) {
|
|
66
|
+
const capabilities = prepareClientCapabilities(input.clientCapabilities);
|
|
67
|
+
const output = {
|
|
68
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
69
|
+
clientCapabilities: capabilities,
|
|
70
|
+
};
|
|
71
|
+
if (input.clientInfo !== undefined) {
|
|
72
|
+
output['clientInfo'] = validateImplementation(input.clientInfo, 'client implementation', 'invalid_params');
|
|
73
|
+
}
|
|
74
|
+
attachMeta(output, input._meta, 'invalid_params');
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
/** Parse and normalize one stable initialize response. */
|
|
78
|
+
export function parseInitializeResult(value) {
|
|
79
|
+
const response = messageRecord(value, 'initialize response');
|
|
80
|
+
const protocolVersion = response['protocolVersion'];
|
|
81
|
+
if (protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
82
|
+
if (nonNegativeInteger(protocolVersion) && protocolVersion <= 65_535) {
|
|
83
|
+
throw acpError('unsupported_protocol_version', 'The ACP peer selected an unsupported protocol version.');
|
|
84
|
+
}
|
|
85
|
+
throw invalidMessage('initialize response');
|
|
86
|
+
}
|
|
87
|
+
const capabilities = parseAgentCapabilities(response['agentCapabilities']);
|
|
88
|
+
const authMethods = response['authMethods'];
|
|
89
|
+
if (authMethods !== undefined && !Array.isArray(authMethods)) {
|
|
90
|
+
throw invalidMessage('initialize response');
|
|
91
|
+
}
|
|
92
|
+
const output = {
|
|
93
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
94
|
+
capabilities,
|
|
95
|
+
authMethods: authMethods ?? [],
|
|
96
|
+
};
|
|
97
|
+
if (response['agentInfo'] !== undefined && response['agentInfo'] !== null) {
|
|
98
|
+
output.agentInfo = validateImplementation(response['agentInfo'], 'agent implementation', 'invalid_message');
|
|
99
|
+
}
|
|
100
|
+
attachParsedMeta(output, response, 'initialize response');
|
|
101
|
+
return output;
|
|
102
|
+
}
|
|
103
|
+
/** Validate a session/new request before it reaches the wire. */
|
|
104
|
+
export function prepareNewSessionInput(input, capabilities) {
|
|
105
|
+
return prepareSessionConnectionInput(input, capabilities, true);
|
|
106
|
+
}
|
|
107
|
+
/** Validate a session/load request before it reaches the wire. */
|
|
108
|
+
export function prepareLoadSessionInput(input, capabilities) {
|
|
109
|
+
const output = prepareSessionConnectionInput(input, capabilities, true);
|
|
110
|
+
return { ...output, sessionId: identifier(input.sessionId, 'session ID') };
|
|
111
|
+
}
|
|
112
|
+
/** Validate a session/resume request before it reaches the wire. */
|
|
113
|
+
export function prepareResumeSessionInput(input, capabilities) {
|
|
114
|
+
const output = prepareSessionConnectionInput(input, capabilities, false);
|
|
115
|
+
return { ...output, sessionId: identifier(input.sessionId, 'session ID') };
|
|
116
|
+
}
|
|
117
|
+
/** Parse a session/new response. */
|
|
118
|
+
export function parseNewSessionResult(value) {
|
|
119
|
+
const response = messageRecord(value, 'session/new response');
|
|
120
|
+
return {
|
|
121
|
+
sessionId: inboundIdentifier(response['sessionId'], 'session/new response'),
|
|
122
|
+
...parseSessionState(response, 'session/new response'),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Parse a session/load or session/resume response. */
|
|
126
|
+
export function parseSessionStateResult(value) {
|
|
127
|
+
return parseSessionState(messageRecord(value, 'session setup response'), 'session setup response');
|
|
128
|
+
}
|
|
129
|
+
/** Validate a session/list request. */
|
|
130
|
+
export function prepareListSessionsInput(input = {}) {
|
|
131
|
+
const output = {};
|
|
132
|
+
if (input.cwd !== undefined && input.cwd !== null) {
|
|
133
|
+
output['cwd'] = absolutePath(input.cwd, 'session/list cwd');
|
|
134
|
+
}
|
|
135
|
+
else if (input.cwd === null) {
|
|
136
|
+
output['cwd'] = null;
|
|
137
|
+
}
|
|
138
|
+
if (input.cursor !== undefined && input.cursor !== null) {
|
|
139
|
+
output['cursor'] = identifier(input.cursor, 'session/list cursor');
|
|
140
|
+
}
|
|
141
|
+
else if (input.cursor === null) {
|
|
142
|
+
output['cursor'] = null;
|
|
143
|
+
}
|
|
144
|
+
attachMeta(output, input._meta, 'invalid_params');
|
|
145
|
+
return output;
|
|
146
|
+
}
|
|
147
|
+
/** Parse a session/list response. */
|
|
148
|
+
export function parseListSessionsResult(value) {
|
|
149
|
+
const response = messageRecord(value, 'session/list response');
|
|
150
|
+
const sessions = response['sessions'];
|
|
151
|
+
if (!Array.isArray(sessions))
|
|
152
|
+
throw invalidMessage('session/list response');
|
|
153
|
+
const output = { sessions: sessions.map(parseSessionInfo) };
|
|
154
|
+
if (response['nextCursor'] !== undefined) {
|
|
155
|
+
const nextCursor = response['nextCursor'];
|
|
156
|
+
if (nextCursor !== null && typeof nextCursor !== 'string') {
|
|
157
|
+
throw invalidMessage('session/list response');
|
|
158
|
+
}
|
|
159
|
+
output.nextCursor = nextCursor;
|
|
160
|
+
}
|
|
161
|
+
attachParsedMeta(output, response, 'session/list response');
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
/** Parse an empty object response while retaining only ACP metadata. */
|
|
165
|
+
export function parseEmptyResult(value) {
|
|
166
|
+
const response = messageRecord(value, 'empty ACP response');
|
|
167
|
+
const output = {};
|
|
168
|
+
attachParsedMeta(output, response, 'empty ACP response');
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
/** Validate a session/prompt request against negotiated content capabilities. */
|
|
172
|
+
export function preparePromptInput(input, capabilities) {
|
|
173
|
+
const sessionId = identifier(input.sessionId, 'session ID');
|
|
174
|
+
if (!Array.isArray(input.prompt) || input.prompt.length === 0) {
|
|
175
|
+
throw invalidParams('ACP prompts must contain at least one content block.');
|
|
176
|
+
}
|
|
177
|
+
for (const block of input.prompt)
|
|
178
|
+
validateContentBlock(block, capabilities);
|
|
179
|
+
const output = { sessionId, prompt: input.prompt };
|
|
180
|
+
attachMeta(output, input._meta, 'invalid_params');
|
|
181
|
+
return output;
|
|
182
|
+
}
|
|
183
|
+
/** Parse the authoritative terminal result for one prompt turn. */
|
|
184
|
+
export function parsePromptResult(value) {
|
|
185
|
+
const response = messageRecord(value, 'session/prompt response');
|
|
186
|
+
const reason = response['stopReason'];
|
|
187
|
+
if (typeof reason !== 'string' || !stopReasons.has(reason)) {
|
|
188
|
+
throw invalidMessage('session/prompt response');
|
|
189
|
+
}
|
|
190
|
+
const output = {
|
|
191
|
+
stopReason: reason,
|
|
192
|
+
};
|
|
193
|
+
attachParsedMeta(output, response, 'session/prompt response');
|
|
194
|
+
return output;
|
|
195
|
+
}
|
|
196
|
+
/** Parse one session/update notification, preserving future variants safely. */
|
|
197
|
+
export function parseSessionNotification(value) {
|
|
198
|
+
const params = messageRecord(value, 'session/update notification');
|
|
199
|
+
const sessionId = inboundIdentifier(params['sessionId'], 'session/update notification');
|
|
200
|
+
const update = messageRecord(params['update'], 'session/update notification');
|
|
201
|
+
const tag = update['sessionUpdate'];
|
|
202
|
+
if (typeof tag !== 'string' || tag.length === 0) {
|
|
203
|
+
throw invalidMessage('session/update notification');
|
|
204
|
+
}
|
|
205
|
+
if (!knownSessionUpdate(tag)) {
|
|
206
|
+
return {
|
|
207
|
+
kind: 'unknown',
|
|
208
|
+
observation: redactAcpObservation('unknown_session_update', 'session/update', params),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { kind: 'update', sessionId, update: parseKnownUpdate(update, tag) };
|
|
212
|
+
}
|
|
213
|
+
/** Parse one bidirectional permission request. */
|
|
214
|
+
export function parsePermissionRequest(value) {
|
|
215
|
+
const request = messageRecord(value, 'permission request');
|
|
216
|
+
const sessionId = inboundIdentifier(request['sessionId'], 'permission request');
|
|
217
|
+
const toolCall = parseToolCallUpdate(request['toolCall'], 'permission request');
|
|
218
|
+
const options = request['options'];
|
|
219
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
220
|
+
throw invalidMessage('permission request');
|
|
221
|
+
}
|
|
222
|
+
const parsedOptions = options.map((option) => {
|
|
223
|
+
const item = messageRecord(option, 'permission option');
|
|
224
|
+
const kind = item['kind'];
|
|
225
|
+
if (kind !== 'allow_once' &&
|
|
226
|
+
kind !== 'allow_always' &&
|
|
227
|
+
kind !== 'reject_once' &&
|
|
228
|
+
kind !== 'reject_always') {
|
|
229
|
+
throw invalidMessage('permission option');
|
|
230
|
+
}
|
|
231
|
+
const output = {
|
|
232
|
+
optionId: inboundIdentifier(item['optionId'], 'permission option'),
|
|
233
|
+
name: inboundString(item['name'], 'permission option'),
|
|
234
|
+
kind,
|
|
235
|
+
};
|
|
236
|
+
attachParsedMeta(output, item, 'permission option');
|
|
237
|
+
return output;
|
|
238
|
+
});
|
|
239
|
+
const output = { sessionId, toolCall, options: parsedOptions };
|
|
240
|
+
attachParsedMeta(output, request, 'permission request');
|
|
241
|
+
return output;
|
|
242
|
+
}
|
|
243
|
+
/** Validate a host decision before replying to a permission request. */
|
|
244
|
+
export function preparePermissionOutcome(value, request) {
|
|
245
|
+
if (value.outcome === 'cancelled') {
|
|
246
|
+
const outcome = { outcome: 'cancelled' };
|
|
247
|
+
attachMeta(outcome, value._meta, 'invalid_params');
|
|
248
|
+
return { outcome };
|
|
249
|
+
}
|
|
250
|
+
const optionId = identifier(value.optionId, 'permission option ID');
|
|
251
|
+
if (!request.options.some((option) => option.optionId === optionId)) {
|
|
252
|
+
throw invalidParams('The selected permission option was not advertised.');
|
|
253
|
+
}
|
|
254
|
+
const outcome = { outcome: 'selected', optionId };
|
|
255
|
+
attachMeta(outcome, value._meta, 'invalid_params');
|
|
256
|
+
return { outcome };
|
|
257
|
+
}
|
|
258
|
+
/** Produce a bounded structural view of unknown ACP traffic. */
|
|
259
|
+
export function redactAcpObservation(kind, method, params) {
|
|
260
|
+
return {
|
|
261
|
+
kind,
|
|
262
|
+
method: publicMethod(method),
|
|
263
|
+
params: redact(params, 0, { nodes: 0 }),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Require a valid extension method name reserved by ACP. */
|
|
267
|
+
export function extensionMethod(method) {
|
|
268
|
+
if (typeof method !== 'string' ||
|
|
269
|
+
!/^_[\u0021-\u007e]{0,255}$/u.test(method)) {
|
|
270
|
+
throw invalidParams('ACP extension methods must start with an underscore.');
|
|
271
|
+
}
|
|
272
|
+
return method;
|
|
273
|
+
}
|
|
274
|
+
/** Validate a public Session identifier for an outbound method. */
|
|
275
|
+
export function sessionIdentifier(value) {
|
|
276
|
+
return identifier(value, 'session ID');
|
|
277
|
+
}
|
|
278
|
+
function prepareClientCapabilities(value) {
|
|
279
|
+
const fs = value?.fs;
|
|
280
|
+
const auth = value?.auth;
|
|
281
|
+
if (fs?.readTextFile === true ||
|
|
282
|
+
fs?.writeTextFile === true ||
|
|
283
|
+
value?.terminal === true ||
|
|
284
|
+
auth?.terminal === true) {
|
|
285
|
+
throw acpError('invalid_configuration', 'This ACP client profile cannot advertise unimplemented client services.');
|
|
286
|
+
}
|
|
287
|
+
assertOptionalBoolean(fs?.readTextFile, 'client fs capability');
|
|
288
|
+
assertOptionalBoolean(fs?.writeTextFile, 'client fs capability');
|
|
289
|
+
assertOptionalBoolean(value?.terminal, 'client terminal capability');
|
|
290
|
+
assertOptionalBoolean(auth?.terminal, 'client auth capability');
|
|
291
|
+
const fsOutput = {
|
|
292
|
+
readTextFile: false,
|
|
293
|
+
writeTextFile: false,
|
|
294
|
+
};
|
|
295
|
+
attachMeta(fsOutput, fs?._meta, 'invalid_params');
|
|
296
|
+
const authOutput = { terminal: false };
|
|
297
|
+
attachMeta(authOutput, auth?._meta, 'invalid_params');
|
|
298
|
+
const output = {
|
|
299
|
+
fs: fsOutput,
|
|
300
|
+
terminal: false,
|
|
301
|
+
auth: authOutput,
|
|
302
|
+
};
|
|
303
|
+
attachMeta(output, value?._meta, 'invalid_params');
|
|
304
|
+
return output;
|
|
305
|
+
}
|
|
306
|
+
function parseAgentCapabilities(value) {
|
|
307
|
+
const capabilities = value === undefined ? {} : messageRecord(value, 'agent capabilities');
|
|
308
|
+
const prompt = optionalRecord(capabilities, 'promptCapabilities', 'agent capabilities');
|
|
309
|
+
const mcp = optionalRecord(capabilities, 'mcpCapabilities', 'agent capabilities');
|
|
310
|
+
const session = optionalRecord(capabilities, 'sessionCapabilities', 'agent capabilities');
|
|
311
|
+
const output = {
|
|
312
|
+
loadSession: optionalBoolean(capabilities, 'loadSession', 'agent capabilities'),
|
|
313
|
+
prompt: {
|
|
314
|
+
image: optionalBoolean(prompt, 'image', 'prompt capabilities'),
|
|
315
|
+
audio: optionalBoolean(prompt, 'audio', 'prompt capabilities'),
|
|
316
|
+
embeddedContext: optionalBoolean(prompt, 'embeddedContext', 'prompt capabilities'),
|
|
317
|
+
},
|
|
318
|
+
mcp: {
|
|
319
|
+
http: optionalBoolean(mcp, 'http', 'MCP capabilities'),
|
|
320
|
+
sse: optionalBoolean(mcp, 'sse', 'MCP capabilities'),
|
|
321
|
+
},
|
|
322
|
+
session: {
|
|
323
|
+
list: optionalMarker(session, 'list', 'session capabilities'),
|
|
324
|
+
delete: optionalMarker(session, 'delete', 'session capabilities'),
|
|
325
|
+
additionalDirectories: optionalMarker(session, 'additionalDirectories', 'session capabilities'),
|
|
326
|
+
resume: optionalMarker(session, 'resume', 'session capabilities'),
|
|
327
|
+
close: optionalMarker(session, 'close', 'session capabilities'),
|
|
328
|
+
},
|
|
329
|
+
...metaProperty(capabilities, 'agent capabilities'),
|
|
330
|
+
};
|
|
331
|
+
return output;
|
|
332
|
+
}
|
|
333
|
+
function prepareSessionConnectionInput(input, capabilities, requireMcpServers) {
|
|
334
|
+
const cwd = absolutePath(input.cwd, 'session cwd');
|
|
335
|
+
const mcpServers = input.mcpServers;
|
|
336
|
+
if (requireMcpServers && !Array.isArray(mcpServers)) {
|
|
337
|
+
throw invalidParams('ACP session setup requires an MCP server array.');
|
|
338
|
+
}
|
|
339
|
+
if (mcpServers !== undefined) {
|
|
340
|
+
if (!Array.isArray(mcpServers)) {
|
|
341
|
+
throw invalidParams('ACP MCP servers must be an array.');
|
|
342
|
+
}
|
|
343
|
+
for (const server of mcpServers)
|
|
344
|
+
validateMcpServer(server, capabilities);
|
|
345
|
+
}
|
|
346
|
+
const additionalDirectories = input.additionalDirectories;
|
|
347
|
+
if (additionalDirectories !== undefined) {
|
|
348
|
+
if (!Array.isArray(additionalDirectories)) {
|
|
349
|
+
throw invalidParams('ACP additional directories must be an array.');
|
|
350
|
+
}
|
|
351
|
+
if (additionalDirectories.length > 0 &&
|
|
352
|
+
!capabilities.session.additionalDirectories) {
|
|
353
|
+
throw acpError('capability_not_advertised', 'The ACP Agent did not advertise additional directory support.');
|
|
354
|
+
}
|
|
355
|
+
for (const directory of additionalDirectories) {
|
|
356
|
+
absolutePath(directory, 'additional directory');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const output = { cwd };
|
|
360
|
+
if (mcpServers !== undefined)
|
|
361
|
+
output['mcpServers'] = mcpServers;
|
|
362
|
+
if (additionalDirectories !== undefined) {
|
|
363
|
+
output['additionalDirectories'] = additionalDirectories;
|
|
364
|
+
}
|
|
365
|
+
attachMeta(output, input._meta, 'invalid_params');
|
|
366
|
+
return output;
|
|
367
|
+
}
|
|
368
|
+
function validateMcpServer(value, capabilities) {
|
|
369
|
+
const server = localRecord(value, 'MCP server');
|
|
370
|
+
identifier(server['name'], 'MCP server name');
|
|
371
|
+
if (server['type'] === 'http' || server['type'] === 'sse') {
|
|
372
|
+
if (!capabilities.mcp[server['type']]) {
|
|
373
|
+
throw acpError('capability_not_advertised', `The ACP Agent did not advertise ${server['type'].toUpperCase()} MCP support.`);
|
|
374
|
+
}
|
|
375
|
+
httpUrl(server['url'], 'MCP server URL');
|
|
376
|
+
validateNameValueArray(server['headers'], 'MCP headers');
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
absolutePath(server['command'], 'MCP command');
|
|
380
|
+
stringArray(server['args'], 'MCP arguments', false);
|
|
381
|
+
validateNameValueArray(server['env'], 'MCP environment');
|
|
382
|
+
}
|
|
383
|
+
validateMeta(server['_meta'], 'invalid_params');
|
|
384
|
+
}
|
|
385
|
+
function parseSessionState(response, surface) {
|
|
386
|
+
const output = {};
|
|
387
|
+
if (response['modes'] !== undefined) {
|
|
388
|
+
const modes = response['modes'];
|
|
389
|
+
output.modes =
|
|
390
|
+
modes === null ? null : parseSessionModeState(modes, surface);
|
|
391
|
+
}
|
|
392
|
+
if (response['configOptions'] !== undefined) {
|
|
393
|
+
const options = response['configOptions'];
|
|
394
|
+
output.configOptions =
|
|
395
|
+
options === null ? null : parseSessionConfigOptions(options, surface);
|
|
396
|
+
}
|
|
397
|
+
attachParsedMeta(output, response, surface);
|
|
398
|
+
return output;
|
|
399
|
+
}
|
|
400
|
+
function parseSessionInfo(value) {
|
|
401
|
+
const session = messageRecord(value, 'session/list entry');
|
|
402
|
+
const output = {
|
|
403
|
+
sessionId: inboundIdentifier(session['sessionId'], 'session/list entry'),
|
|
404
|
+
cwd: inboundAbsolutePath(session['cwd'], 'session/list entry'),
|
|
405
|
+
};
|
|
406
|
+
if (session['additionalDirectories'] !== undefined) {
|
|
407
|
+
output.additionalDirectories = inboundStringArray(session['additionalDirectories'], 'session/list entry').map((directory) => inboundAbsolutePath(directory, 'session/list entry'));
|
|
408
|
+
}
|
|
409
|
+
optionalNullableString(output, session, 'title', 'session/list entry');
|
|
410
|
+
optionalNullableString(output, session, 'updatedAt', 'session/list entry');
|
|
411
|
+
attachParsedMeta(output, session, 'session/list entry');
|
|
412
|
+
return output;
|
|
413
|
+
}
|
|
414
|
+
function validateContentBlock(value, capabilities) {
|
|
415
|
+
const block = localRecord(value, 'content block');
|
|
416
|
+
validateLocalAnnotations(block['annotations'], 'content annotations');
|
|
417
|
+
validateMeta(block['_meta'], 'invalid_params');
|
|
418
|
+
switch (block['type']) {
|
|
419
|
+
case 'text':
|
|
420
|
+
boundedString(block['text'], 'text content');
|
|
421
|
+
break;
|
|
422
|
+
case 'image':
|
|
423
|
+
if (!capabilities.prompt.image)
|
|
424
|
+
capabilityContent('image');
|
|
425
|
+
boundedString(block['data'], 'image data');
|
|
426
|
+
boundedString(block['mimeType'], 'image MIME type');
|
|
427
|
+
validateOptionalLocalString(block, 'uri', 'image URI');
|
|
428
|
+
break;
|
|
429
|
+
case 'audio':
|
|
430
|
+
if (!capabilities.prompt.audio)
|
|
431
|
+
capabilityContent('audio');
|
|
432
|
+
boundedString(block['data'], 'audio data');
|
|
433
|
+
boundedString(block['mimeType'], 'audio MIME type');
|
|
434
|
+
break;
|
|
435
|
+
case 'resource_link': {
|
|
436
|
+
boundedString(block['name'], 'resource name');
|
|
437
|
+
boundedString(block['uri'], 'resource URI');
|
|
438
|
+
validateOptionalLocalString(block, 'title', 'resource title');
|
|
439
|
+
validateOptionalLocalString(block, 'description', 'resource description');
|
|
440
|
+
validateOptionalLocalString(block, 'mimeType', 'resource MIME type');
|
|
441
|
+
const size = block['size'];
|
|
442
|
+
if (size !== undefined &&
|
|
443
|
+
size !== null &&
|
|
444
|
+
(!Number.isSafeInteger(size) || Number(size) < 0)) {
|
|
445
|
+
throw invalidParams('The ACP resource size is invalid.');
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case 'resource': {
|
|
450
|
+
if (!capabilities.prompt.embeddedContext)
|
|
451
|
+
capabilityContent('embedded context');
|
|
452
|
+
const resource = localRecord(block['resource'], 'embedded resource');
|
|
453
|
+
boundedString(resource['uri'], 'embedded resource URI');
|
|
454
|
+
const hasText = typeof resource['text'] === 'string';
|
|
455
|
+
const hasBlob = typeof resource['blob'] === 'string';
|
|
456
|
+
if (hasText === hasBlob) {
|
|
457
|
+
throw invalidParams('Embedded resources require exactly one payload.');
|
|
458
|
+
}
|
|
459
|
+
boundedString(hasText ? resource['text'] : resource['blob'], 'embedded resource payload');
|
|
460
|
+
validateOptionalLocalString(resource, 'mimeType', 'embedded resource MIME type');
|
|
461
|
+
validateMeta(resource['_meta'], 'invalid_params');
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
default:
|
|
465
|
+
throw invalidParams('The ACP content block type is invalid.');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function parseKnownUpdate(update, tag) {
|
|
469
|
+
switch (tag) {
|
|
470
|
+
case 'user_message_chunk':
|
|
471
|
+
case 'agent_message_chunk':
|
|
472
|
+
case 'agent_thought_chunk': {
|
|
473
|
+
const content = parseInboundContentBlock(update['content']);
|
|
474
|
+
const output = { sessionUpdate: tag, content };
|
|
475
|
+
optionalNullableString(output, update, 'messageId', 'content chunk');
|
|
476
|
+
attachParsedMeta(output, update, 'content chunk');
|
|
477
|
+
return output;
|
|
478
|
+
}
|
|
479
|
+
case 'tool_call': {
|
|
480
|
+
const toolCall = parseToolCallUpdate(update, 'tool call');
|
|
481
|
+
const title = inboundString(update['title'], 'tool call');
|
|
482
|
+
return { ...toolCall, sessionUpdate: tag, title };
|
|
483
|
+
}
|
|
484
|
+
case 'tool_call_update':
|
|
485
|
+
return {
|
|
486
|
+
...parseToolCallUpdate(update, 'tool call update'),
|
|
487
|
+
sessionUpdate: tag,
|
|
488
|
+
};
|
|
489
|
+
case 'plan':
|
|
490
|
+
return {
|
|
491
|
+
sessionUpdate: tag,
|
|
492
|
+
entries: parsePlanEntries(update['entries']),
|
|
493
|
+
...metaProperty(update, 'plan update'),
|
|
494
|
+
};
|
|
495
|
+
case 'available_commands_update':
|
|
496
|
+
return {
|
|
497
|
+
sessionUpdate: tag,
|
|
498
|
+
availableCommands: parseAvailableCommands(update['availableCommands']),
|
|
499
|
+
...metaProperty(update, 'available commands update'),
|
|
500
|
+
};
|
|
501
|
+
case 'current_mode_update':
|
|
502
|
+
return {
|
|
503
|
+
sessionUpdate: tag,
|
|
504
|
+
currentModeId: inboundIdentifier(update['currentModeId'], 'mode update'),
|
|
505
|
+
...metaProperty(update, 'mode update'),
|
|
506
|
+
};
|
|
507
|
+
case 'config_option_update':
|
|
508
|
+
return {
|
|
509
|
+
sessionUpdate: tag,
|
|
510
|
+
configOptions: parseSessionConfigOptions(update['configOptions'], 'config option update'),
|
|
511
|
+
...metaProperty(update, 'config option update'),
|
|
512
|
+
};
|
|
513
|
+
case 'session_info_update': {
|
|
514
|
+
const output = { sessionUpdate: tag };
|
|
515
|
+
optionalNullableString(output, update, 'title', 'session info update');
|
|
516
|
+
optionalNullableString(output, update, 'updatedAt', 'session info update');
|
|
517
|
+
attachParsedMeta(output, update, 'session info update');
|
|
518
|
+
return output;
|
|
519
|
+
}
|
|
520
|
+
case 'usage_update': {
|
|
521
|
+
const used = nonNegativeSafeInteger(update['used'], 'usage update');
|
|
522
|
+
const size = nonNegativeSafeInteger(update['size'], 'usage update');
|
|
523
|
+
const output = { sessionUpdate: tag, used, size };
|
|
524
|
+
if (update['cost'] !== undefined) {
|
|
525
|
+
const cost = update['cost'];
|
|
526
|
+
if (cost === null)
|
|
527
|
+
output.cost = null;
|
|
528
|
+
else {
|
|
529
|
+
const costRecord = messageRecord(cost, 'usage cost');
|
|
530
|
+
const amount = costRecord['amount'];
|
|
531
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount)) {
|
|
532
|
+
throw invalidMessage('usage cost');
|
|
533
|
+
}
|
|
534
|
+
output.cost = {
|
|
535
|
+
amount,
|
|
536
|
+
currency: inboundString(costRecord['currency'], 'usage cost'),
|
|
537
|
+
...metaProperty(costRecord, 'usage cost'),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
attachParsedMeta(output, update, 'usage update');
|
|
542
|
+
return output;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function parseInboundContentBlock(value) {
|
|
547
|
+
const block = messageRecord(value, 'content block');
|
|
548
|
+
const type = block['type'];
|
|
549
|
+
switch (type) {
|
|
550
|
+
case 'text':
|
|
551
|
+
return {
|
|
552
|
+
type,
|
|
553
|
+
text: inboundString(block['text'], 'content block'),
|
|
554
|
+
...annotationsProperty(block, 'content block'),
|
|
555
|
+
...metaProperty(block, 'content block'),
|
|
556
|
+
};
|
|
557
|
+
case 'image':
|
|
558
|
+
return {
|
|
559
|
+
type,
|
|
560
|
+
data: inboundString(block['data'], 'content block'),
|
|
561
|
+
mimeType: inboundString(block['mimeType'], 'content block'),
|
|
562
|
+
...nullableStringProperty(block, 'uri', 'content block'),
|
|
563
|
+
...annotationsProperty(block, 'content block'),
|
|
564
|
+
...metaProperty(block, 'content block'),
|
|
565
|
+
};
|
|
566
|
+
case 'audio':
|
|
567
|
+
return {
|
|
568
|
+
type,
|
|
569
|
+
data: inboundString(block['data'], 'content block'),
|
|
570
|
+
mimeType: inboundString(block['mimeType'], 'content block'),
|
|
571
|
+
...annotationsProperty(block, 'content block'),
|
|
572
|
+
...metaProperty(block, 'content block'),
|
|
573
|
+
};
|
|
574
|
+
case 'resource_link': {
|
|
575
|
+
const size = block['size'];
|
|
576
|
+
if (size !== undefined &&
|
|
577
|
+
size !== null &&
|
|
578
|
+
(!Number.isSafeInteger(size) || Number(size) < 0)) {
|
|
579
|
+
throw invalidMessage('content block');
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
type,
|
|
583
|
+
name: inboundString(block['name'], 'content block'),
|
|
584
|
+
uri: inboundString(block['uri'], 'content block'),
|
|
585
|
+
...nullableStringProperty(block, 'title', 'content block'),
|
|
586
|
+
...nullableStringProperty(block, 'description', 'content block'),
|
|
587
|
+
...nullableStringProperty(block, 'mimeType', 'content block'),
|
|
588
|
+
...(size === undefined ? {} : { size: size }),
|
|
589
|
+
...annotationsProperty(block, 'content block'),
|
|
590
|
+
...metaProperty(block, 'content block'),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
case 'resource': {
|
|
594
|
+
const resource = messageRecord(block['resource'], 'embedded resource');
|
|
595
|
+
const uri = inboundString(resource['uri'], 'embedded resource');
|
|
596
|
+
const hasText = typeof resource['text'] === 'string';
|
|
597
|
+
const hasBlob = typeof resource['blob'] === 'string';
|
|
598
|
+
if (hasText === hasBlob)
|
|
599
|
+
throw invalidMessage('embedded resource');
|
|
600
|
+
const shared = {
|
|
601
|
+
uri,
|
|
602
|
+
...nullableStringProperty(resource, 'mimeType', 'embedded resource'),
|
|
603
|
+
...metaProperty(resource, 'embedded resource'),
|
|
604
|
+
};
|
|
605
|
+
return {
|
|
606
|
+
type,
|
|
607
|
+
resource: hasText
|
|
608
|
+
? {
|
|
609
|
+
...shared,
|
|
610
|
+
text: inboundString(resource['text'], 'embedded resource'),
|
|
611
|
+
}
|
|
612
|
+
: {
|
|
613
|
+
...shared,
|
|
614
|
+
blob: inboundString(resource['blob'], 'embedded resource'),
|
|
615
|
+
},
|
|
616
|
+
...annotationsProperty(block, 'content block'),
|
|
617
|
+
...metaProperty(block, 'content block'),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
default:
|
|
621
|
+
throw invalidMessage('content block');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function parseToolCallUpdate(value, surface) {
|
|
625
|
+
const tool = messageRecord(value, surface);
|
|
626
|
+
const output = { toolCallId: inboundIdentifier(tool['toolCallId'], surface) };
|
|
627
|
+
optionalNullableString(output, tool, 'title', surface);
|
|
628
|
+
if (tool['kind'] !== undefined) {
|
|
629
|
+
const kind = tool['kind'];
|
|
630
|
+
if (kind !== null &&
|
|
631
|
+
(typeof kind !== 'string' || !toolKinds.has(kind))) {
|
|
632
|
+
throw invalidMessage(surface);
|
|
633
|
+
}
|
|
634
|
+
output.kind = kind;
|
|
635
|
+
}
|
|
636
|
+
if (tool['status'] !== undefined) {
|
|
637
|
+
const status = tool['status'];
|
|
638
|
+
if (status !== null &&
|
|
639
|
+
(typeof status !== 'string' ||
|
|
640
|
+
!toolStatuses.has(status))) {
|
|
641
|
+
throw invalidMessage(surface);
|
|
642
|
+
}
|
|
643
|
+
output.status = status;
|
|
644
|
+
}
|
|
645
|
+
if (tool['content'] !== undefined) {
|
|
646
|
+
const content = tool['content'];
|
|
647
|
+
if (content !== null && !Array.isArray(content))
|
|
648
|
+
throw invalidMessage(surface);
|
|
649
|
+
output.content =
|
|
650
|
+
content === null
|
|
651
|
+
? null
|
|
652
|
+
: content.map((item) => parseToolCallContent(item, surface));
|
|
653
|
+
}
|
|
654
|
+
if (tool['locations'] !== undefined) {
|
|
655
|
+
const locations = tool['locations'];
|
|
656
|
+
if (locations !== null && !Array.isArray(locations))
|
|
657
|
+
throw invalidMessage(surface);
|
|
658
|
+
output.locations =
|
|
659
|
+
locations === null
|
|
660
|
+
? null
|
|
661
|
+
: locations.map((item) => parseToolCallLocation(item, surface));
|
|
662
|
+
}
|
|
663
|
+
if (Object.hasOwn(tool, 'rawInput'))
|
|
664
|
+
output.rawInput = tool['rawInput'];
|
|
665
|
+
if (Object.hasOwn(tool, 'rawOutput'))
|
|
666
|
+
output.rawOutput = tool['rawOutput'];
|
|
667
|
+
attachParsedMeta(output, tool, surface);
|
|
668
|
+
return output;
|
|
669
|
+
}
|
|
670
|
+
function parseToolCallContent(value, surface) {
|
|
671
|
+
const content = messageRecord(value, surface);
|
|
672
|
+
switch (content['type']) {
|
|
673
|
+
case 'content':
|
|
674
|
+
return {
|
|
675
|
+
type: 'content',
|
|
676
|
+
content: parseInboundContentBlock(content['content']),
|
|
677
|
+
...metaProperty(content, surface),
|
|
678
|
+
};
|
|
679
|
+
case 'diff': {
|
|
680
|
+
const output = {
|
|
681
|
+
type: 'diff',
|
|
682
|
+
path: inboundAbsolutePath(content['path'], surface),
|
|
683
|
+
newText: inboundString(content['newText'], surface),
|
|
684
|
+
};
|
|
685
|
+
optionalNullableString(output, content, 'oldText', surface);
|
|
686
|
+
attachParsedMeta(output, content, surface);
|
|
687
|
+
return output;
|
|
688
|
+
}
|
|
689
|
+
case 'terminal':
|
|
690
|
+
return {
|
|
691
|
+
type: 'terminal',
|
|
692
|
+
terminalId: inboundIdentifier(content['terminalId'], surface),
|
|
693
|
+
...metaProperty(content, surface),
|
|
694
|
+
};
|
|
695
|
+
default:
|
|
696
|
+
throw invalidMessage(surface);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function parseToolCallLocation(value, surface) {
|
|
700
|
+
const location = messageRecord(value, surface);
|
|
701
|
+
const output = { path: inboundAbsolutePath(location['path'], surface) };
|
|
702
|
+
if (location['line'] !== undefined) {
|
|
703
|
+
const line = location['line'];
|
|
704
|
+
if (line !== null && !unsigned32BitInteger(line)) {
|
|
705
|
+
throw invalidMessage(surface);
|
|
706
|
+
}
|
|
707
|
+
output.line = line;
|
|
708
|
+
}
|
|
709
|
+
attachParsedMeta(output, location, surface);
|
|
710
|
+
return output;
|
|
711
|
+
}
|
|
712
|
+
function parsePlanEntries(value) {
|
|
713
|
+
if (!Array.isArray(value))
|
|
714
|
+
throw invalidMessage('plan update');
|
|
715
|
+
return value.map((entry) => {
|
|
716
|
+
const item = messageRecord(entry, 'plan entry');
|
|
717
|
+
const priority = item['priority'];
|
|
718
|
+
const status = item['status'];
|
|
719
|
+
if (priority !== 'high' && priority !== 'medium' && priority !== 'low') {
|
|
720
|
+
throw invalidMessage('plan entry');
|
|
721
|
+
}
|
|
722
|
+
if (status !== 'pending' &&
|
|
723
|
+
status !== 'in_progress' &&
|
|
724
|
+
status !== 'completed') {
|
|
725
|
+
throw invalidMessage('plan entry');
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
content: inboundString(item['content'], 'plan entry'),
|
|
729
|
+
priority,
|
|
730
|
+
status,
|
|
731
|
+
...metaProperty(item, 'plan entry'),
|
|
732
|
+
};
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function parseAvailableCommands(value) {
|
|
736
|
+
if (!Array.isArray(value)) {
|
|
737
|
+
throw invalidMessage('available commands update');
|
|
738
|
+
}
|
|
739
|
+
return value.map((command) => {
|
|
740
|
+
const item = messageRecord(command, 'available command');
|
|
741
|
+
const output = {
|
|
742
|
+
name: inboundString(item['name'], 'available command'),
|
|
743
|
+
description: inboundString(item['description'], 'available command'),
|
|
744
|
+
};
|
|
745
|
+
if (item['input'] !== undefined) {
|
|
746
|
+
const input = item['input'];
|
|
747
|
+
if (input === null)
|
|
748
|
+
output.input = null;
|
|
749
|
+
else {
|
|
750
|
+
const inputRecord = messageRecord(input, 'available command input');
|
|
751
|
+
output.input = {
|
|
752
|
+
hint: inboundString(inputRecord['hint'], 'available command input'),
|
|
753
|
+
...metaProperty(inputRecord, 'available command input'),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
attachParsedMeta(output, item, 'available command');
|
|
758
|
+
return output;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function parseSessionModeState(value, surface) {
|
|
762
|
+
const state = messageRecord(value, surface);
|
|
763
|
+
const availableModes = state['availableModes'];
|
|
764
|
+
if (!Array.isArray(availableModes))
|
|
765
|
+
throw invalidMessage(surface);
|
|
766
|
+
return {
|
|
767
|
+
currentModeId: inboundIdentifier(state['currentModeId'], surface),
|
|
768
|
+
availableModes: availableModes.map((mode) => parseSessionMode(mode, surface)),
|
|
769
|
+
...metaProperty(state, surface),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function parseSessionMode(value, surface) {
|
|
773
|
+
const mode = messageRecord(value, surface);
|
|
774
|
+
const output = {
|
|
775
|
+
id: inboundIdentifier(mode['id'], surface),
|
|
776
|
+
name: inboundString(mode['name'], surface),
|
|
777
|
+
};
|
|
778
|
+
optionalNullableString(output, mode, 'description', surface);
|
|
779
|
+
attachParsedMeta(output, mode, surface);
|
|
780
|
+
return output;
|
|
781
|
+
}
|
|
782
|
+
function parseSessionConfigOptions(value, surface) {
|
|
783
|
+
if (!Array.isArray(value))
|
|
784
|
+
throw invalidMessage(surface);
|
|
785
|
+
return value.map((option) => parseSessionConfigOption(option, surface));
|
|
786
|
+
}
|
|
787
|
+
function parseSessionConfigOption(value, surface) {
|
|
788
|
+
const option = messageRecord(value, surface);
|
|
789
|
+
const shared = {
|
|
790
|
+
id: inboundIdentifier(option['id'], surface),
|
|
791
|
+
name: inboundString(option['name'], surface),
|
|
792
|
+
};
|
|
793
|
+
optionalNullableString(shared, option, 'description', surface);
|
|
794
|
+
optionalNullableString(shared, option, 'category', surface);
|
|
795
|
+
attachParsedMeta(shared, option, surface);
|
|
796
|
+
if (option['type'] === 'boolean') {
|
|
797
|
+
if (typeof option['currentValue'] !== 'boolean') {
|
|
798
|
+
throw invalidMessage(surface);
|
|
799
|
+
}
|
|
800
|
+
return { ...shared, type: 'boolean', currentValue: option['currentValue'] };
|
|
801
|
+
}
|
|
802
|
+
if (option['type'] !== 'select')
|
|
803
|
+
throw invalidMessage(surface);
|
|
804
|
+
return {
|
|
805
|
+
...shared,
|
|
806
|
+
type: 'select',
|
|
807
|
+
currentValue: inboundIdentifier(option['currentValue'], surface),
|
|
808
|
+
options: parseSessionConfigSelectOptions(option['options'], surface),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function parseSessionConfigSelectOptions(value, surface) {
|
|
812
|
+
if (!Array.isArray(value))
|
|
813
|
+
throw invalidMessage(surface);
|
|
814
|
+
if (value.length === 0)
|
|
815
|
+
return [];
|
|
816
|
+
const grouped = value.every((item) => isRecord(item) && Object.hasOwn(item, 'group'));
|
|
817
|
+
const ungrouped = value.every((item) => isRecord(item) && !Object.hasOwn(item, 'group'));
|
|
818
|
+
if (!grouped && !ungrouped)
|
|
819
|
+
throw invalidMessage(surface);
|
|
820
|
+
return grouped
|
|
821
|
+
? value.map((group) => parseSessionConfigSelectGroup(group, surface))
|
|
822
|
+
: value.map((option) => parseSessionConfigSelectOption(option, surface));
|
|
823
|
+
}
|
|
824
|
+
function parseSessionConfigSelectOption(value, surface) {
|
|
825
|
+
const option = messageRecord(value, surface);
|
|
826
|
+
const output = {
|
|
827
|
+
value: inboundIdentifier(option['value'], surface),
|
|
828
|
+
name: inboundString(option['name'], surface),
|
|
829
|
+
};
|
|
830
|
+
optionalNullableString(output, option, 'description', surface);
|
|
831
|
+
attachParsedMeta(output, option, surface);
|
|
832
|
+
return output;
|
|
833
|
+
}
|
|
834
|
+
function parseSessionConfigSelectGroup(value, surface) {
|
|
835
|
+
const group = messageRecord(value, surface);
|
|
836
|
+
const options = group['options'];
|
|
837
|
+
if (!Array.isArray(options))
|
|
838
|
+
throw invalidMessage(surface);
|
|
839
|
+
return {
|
|
840
|
+
group: inboundIdentifier(group['group'], surface),
|
|
841
|
+
name: inboundString(group['name'], surface),
|
|
842
|
+
options: options.map((option) => parseSessionConfigSelectOption(option, surface)),
|
|
843
|
+
...metaProperty(group, surface),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function knownSessionUpdate(value) {
|
|
847
|
+
return [
|
|
848
|
+
'user_message_chunk',
|
|
849
|
+
'agent_message_chunk',
|
|
850
|
+
'agent_thought_chunk',
|
|
851
|
+
'tool_call',
|
|
852
|
+
'tool_call_update',
|
|
853
|
+
'plan',
|
|
854
|
+
'available_commands_update',
|
|
855
|
+
'current_mode_update',
|
|
856
|
+
'config_option_update',
|
|
857
|
+
'session_info_update',
|
|
858
|
+
'usage_update',
|
|
859
|
+
].includes(value);
|
|
860
|
+
}
|
|
861
|
+
function validateImplementation(value, surface, code) {
|
|
862
|
+
const input = code === 'invalid_message'
|
|
863
|
+
? messageRecord(value, surface)
|
|
864
|
+
: localRecord(value, surface);
|
|
865
|
+
const output = {
|
|
866
|
+
name: code === 'invalid_message'
|
|
867
|
+
? inboundString(input['name'], surface)
|
|
868
|
+
: boundedString(input['name'], surface),
|
|
869
|
+
version: code === 'invalid_message'
|
|
870
|
+
? inboundString(input['version'], surface)
|
|
871
|
+
: boundedString(input['version'], surface),
|
|
872
|
+
};
|
|
873
|
+
optionalNullableString(output, input, 'title', surface, code);
|
|
874
|
+
if (code === 'invalid_message')
|
|
875
|
+
attachParsedMeta(output, input, surface);
|
|
876
|
+
else
|
|
877
|
+
attachMeta(output, input['_meta'], code);
|
|
878
|
+
return output;
|
|
879
|
+
}
|
|
880
|
+
function optionalBoolean(record, key, surface) {
|
|
881
|
+
const value = record[key];
|
|
882
|
+
if (value === undefined)
|
|
883
|
+
return false;
|
|
884
|
+
if (typeof value !== 'boolean')
|
|
885
|
+
throw invalidMessage(surface);
|
|
886
|
+
return value;
|
|
887
|
+
}
|
|
888
|
+
function optionalMarker(record, key, surface) {
|
|
889
|
+
const value = record[key];
|
|
890
|
+
if (value === undefined || value === null)
|
|
891
|
+
return false;
|
|
892
|
+
if (!isRecord(value))
|
|
893
|
+
throw invalidMessage(surface);
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
function optionalRecord(record, key, surface) {
|
|
897
|
+
const value = record[key];
|
|
898
|
+
if (value === undefined)
|
|
899
|
+
return {};
|
|
900
|
+
if (!isRecord(value))
|
|
901
|
+
throw invalidMessage(surface);
|
|
902
|
+
return value;
|
|
903
|
+
}
|
|
904
|
+
function annotationsProperty(source, surface) {
|
|
905
|
+
if (!Object.hasOwn(source, 'annotations'))
|
|
906
|
+
return {};
|
|
907
|
+
const value = source['annotations'];
|
|
908
|
+
return {
|
|
909
|
+
annotations: value === null ? null : parseAnnotations(value, surface),
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function parseAnnotations(value, surface) {
|
|
913
|
+
const annotations = messageRecord(value, surface);
|
|
914
|
+
const output = {};
|
|
915
|
+
if (annotations['audience'] !== undefined) {
|
|
916
|
+
const audience = annotations['audience'];
|
|
917
|
+
if (audience !== null &&
|
|
918
|
+
(!Array.isArray(audience) ||
|
|
919
|
+
!audience.every((role) => role === 'assistant' || role === 'user'))) {
|
|
920
|
+
throw invalidMessage(surface);
|
|
921
|
+
}
|
|
922
|
+
output.audience = audience;
|
|
923
|
+
}
|
|
924
|
+
if (annotations['priority'] !== undefined) {
|
|
925
|
+
const priority = annotations['priority'];
|
|
926
|
+
if (priority !== null &&
|
|
927
|
+
(typeof priority !== 'number' || !Number.isFinite(priority))) {
|
|
928
|
+
throw invalidMessage(surface);
|
|
929
|
+
}
|
|
930
|
+
output.priority = priority;
|
|
931
|
+
}
|
|
932
|
+
optionalNullableString(output, annotations, 'lastModified', surface);
|
|
933
|
+
attachParsedMeta(output, annotations, surface);
|
|
934
|
+
return output;
|
|
935
|
+
}
|
|
936
|
+
function validateLocalAnnotations(value, surface) {
|
|
937
|
+
if (value === undefined || value === null)
|
|
938
|
+
return;
|
|
939
|
+
const annotations = localRecord(value, surface);
|
|
940
|
+
const audience = annotations['audience'];
|
|
941
|
+
if (audience !== undefined &&
|
|
942
|
+
audience !== null &&
|
|
943
|
+
(!Array.isArray(audience) ||
|
|
944
|
+
!audience.every((role) => role === 'assistant' || role === 'user'))) {
|
|
945
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
946
|
+
}
|
|
947
|
+
const priority = annotations['priority'];
|
|
948
|
+
if (priority !== undefined &&
|
|
949
|
+
priority !== null &&
|
|
950
|
+
(typeof priority !== 'number' || !Number.isFinite(priority))) {
|
|
951
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
952
|
+
}
|
|
953
|
+
validateOptionalLocalString(annotations, 'lastModified', `${surface} last-modified value`);
|
|
954
|
+
validateMeta(annotations['_meta'], 'invalid_params');
|
|
955
|
+
}
|
|
956
|
+
function validateOptionalLocalString(source, key, surface) {
|
|
957
|
+
const value = source[key];
|
|
958
|
+
if (value !== undefined && value !== null)
|
|
959
|
+
boundedString(value, surface);
|
|
960
|
+
}
|
|
961
|
+
function optionalNullableString(output, source, key, surface, code = 'invalid_message') {
|
|
962
|
+
if (!Object.hasOwn(source, key))
|
|
963
|
+
return;
|
|
964
|
+
const value = source[key];
|
|
965
|
+
if (value !== null && typeof value !== 'string') {
|
|
966
|
+
if (code === 'invalid_message')
|
|
967
|
+
throw invalidMessage(surface);
|
|
968
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
969
|
+
}
|
|
970
|
+
output[key] = value;
|
|
971
|
+
}
|
|
972
|
+
function nullableStringProperty(source, key, surface) {
|
|
973
|
+
const output = {};
|
|
974
|
+
optionalNullableString(output, source, key, surface);
|
|
975
|
+
return output;
|
|
976
|
+
}
|
|
977
|
+
function metaProperty(source, surface) {
|
|
978
|
+
const output = {};
|
|
979
|
+
attachParsedMeta(output, source, surface);
|
|
980
|
+
return output;
|
|
981
|
+
}
|
|
982
|
+
function attachParsedMeta(output, source, surface) {
|
|
983
|
+
if (!Object.hasOwn(source, '_meta'))
|
|
984
|
+
return;
|
|
985
|
+
const value = source['_meta'];
|
|
986
|
+
if (value !== null && !isRecord(value))
|
|
987
|
+
throw invalidMessage(surface);
|
|
988
|
+
output._meta = value;
|
|
989
|
+
}
|
|
990
|
+
function attachMeta(output, value, code) {
|
|
991
|
+
if (value === undefined)
|
|
992
|
+
return;
|
|
993
|
+
validateMeta(value, code);
|
|
994
|
+
output['_meta'] = value;
|
|
995
|
+
}
|
|
996
|
+
function validateMeta(value, code) {
|
|
997
|
+
if (value === undefined)
|
|
998
|
+
return;
|
|
999
|
+
if (value !== null && !isRecord(value)) {
|
|
1000
|
+
if (code === 'invalid_message')
|
|
1001
|
+
throw invalidMessage('ACP metadata');
|
|
1002
|
+
throw invalidParams('ACP metadata must be an object or null.');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function assertOptionalBoolean(value, surface) {
|
|
1006
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
1007
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function validateNameValueArray(value, surface) {
|
|
1011
|
+
if (!Array.isArray(value))
|
|
1012
|
+
throw invalidParams(`ACP ${surface} must be an array.`);
|
|
1013
|
+
for (const item of value) {
|
|
1014
|
+
const entry = localRecord(item, surface);
|
|
1015
|
+
identifier(entry['name'], `${surface} name`);
|
|
1016
|
+
boundedString(entry['value'], `${surface} value`);
|
|
1017
|
+
validateMeta(entry['_meta'], 'invalid_params');
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function stringArray(value, surface, requireNonEmpty) {
|
|
1021
|
+
if (!Array.isArray(value) || (requireNonEmpty && value.length === 0)) {
|
|
1022
|
+
throw invalidParams(`ACP ${surface} must be an array.`);
|
|
1023
|
+
}
|
|
1024
|
+
for (const item of value)
|
|
1025
|
+
boundedString(item, surface);
|
|
1026
|
+
return value;
|
|
1027
|
+
}
|
|
1028
|
+
function inboundStringArray(value, surface) {
|
|
1029
|
+
if (!Array.isArray(value) ||
|
|
1030
|
+
!value.every((item) => typeof item === 'string')) {
|
|
1031
|
+
throw invalidMessage(surface);
|
|
1032
|
+
}
|
|
1033
|
+
return value;
|
|
1034
|
+
}
|
|
1035
|
+
function nonNegativeSafeInteger(value, surface) {
|
|
1036
|
+
if (!nonNegativeInteger(value))
|
|
1037
|
+
throw invalidMessage(surface);
|
|
1038
|
+
return value;
|
|
1039
|
+
}
|
|
1040
|
+
function identifier(value, surface) {
|
|
1041
|
+
if (typeof value !== 'string' ||
|
|
1042
|
+
value.length === 0 ||
|
|
1043
|
+
value.length > maximumIdentifierLength) {
|
|
1044
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
1045
|
+
}
|
|
1046
|
+
return value;
|
|
1047
|
+
}
|
|
1048
|
+
function inboundIdentifier(value, surface) {
|
|
1049
|
+
if (typeof value !== 'string' ||
|
|
1050
|
+
value.length === 0 ||
|
|
1051
|
+
value.length > maximumIdentifierLength) {
|
|
1052
|
+
throw invalidMessage(surface);
|
|
1053
|
+
}
|
|
1054
|
+
return value;
|
|
1055
|
+
}
|
|
1056
|
+
function boundedString(value, surface) {
|
|
1057
|
+
if (typeof value !== 'string' ||
|
|
1058
|
+
value.length === 0 ||
|
|
1059
|
+
value.length > maximumStringLength) {
|
|
1060
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
1061
|
+
}
|
|
1062
|
+
return value;
|
|
1063
|
+
}
|
|
1064
|
+
function inboundString(value, surface) {
|
|
1065
|
+
if (typeof value !== 'string' ||
|
|
1066
|
+
value.length === 0 ||
|
|
1067
|
+
value.length > maximumStringLength) {
|
|
1068
|
+
throw invalidMessage(surface);
|
|
1069
|
+
}
|
|
1070
|
+
return value;
|
|
1071
|
+
}
|
|
1072
|
+
function absolutePath(value, surface) {
|
|
1073
|
+
const path = boundedString(value, surface);
|
|
1074
|
+
if (!isAbsolute(path))
|
|
1075
|
+
throw invalidParams(`The ACP ${surface} must be absolute.`);
|
|
1076
|
+
return path;
|
|
1077
|
+
}
|
|
1078
|
+
function inboundAbsolutePath(value, surface) {
|
|
1079
|
+
const path = inboundString(value, surface);
|
|
1080
|
+
if (!isAbsolute(path))
|
|
1081
|
+
throw invalidMessage(surface);
|
|
1082
|
+
return path;
|
|
1083
|
+
}
|
|
1084
|
+
function httpUrl(value, surface) {
|
|
1085
|
+
const encoded = boundedString(value, surface);
|
|
1086
|
+
let parsed;
|
|
1087
|
+
try {
|
|
1088
|
+
parsed = new URL(encoded);
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
1092
|
+
}
|
|
1093
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
1094
|
+
throw invalidParams(`The ACP ${surface} must use HTTP or HTTPS.`);
|
|
1095
|
+
}
|
|
1096
|
+
return encoded;
|
|
1097
|
+
}
|
|
1098
|
+
function capabilityContent(name) {
|
|
1099
|
+
throw acpError('capability_not_advertised', `The ACP Agent did not advertise ${name} prompt support.`);
|
|
1100
|
+
}
|
|
1101
|
+
function messageRecord(value, surface) {
|
|
1102
|
+
if (!isRecord(value))
|
|
1103
|
+
throw invalidMessage(surface);
|
|
1104
|
+
return value;
|
|
1105
|
+
}
|
|
1106
|
+
function localRecord(value, surface) {
|
|
1107
|
+
if (!isRecord(value))
|
|
1108
|
+
throw invalidParams(`The ACP ${surface} is invalid.`);
|
|
1109
|
+
return value;
|
|
1110
|
+
}
|
|
1111
|
+
function isRecord(value) {
|
|
1112
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1113
|
+
}
|
|
1114
|
+
function nonNegativeInteger(value) {
|
|
1115
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
1116
|
+
}
|
|
1117
|
+
function unsigned32BitInteger(value) {
|
|
1118
|
+
return nonNegativeInteger(value) && value <= 4_294_967_295;
|
|
1119
|
+
}
|
|
1120
|
+
function invalidMessage(surface) {
|
|
1121
|
+
return acpError('invalid_message', `The ACP peer sent an invalid ${surface}.`);
|
|
1122
|
+
}
|
|
1123
|
+
function invalidParams(message) {
|
|
1124
|
+
return acpError('invalid_params', message);
|
|
1125
|
+
}
|
|
1126
|
+
function publicMethod(value) {
|
|
1127
|
+
if (knownMethods.has(value) || /^_[\u0021-\u007e]{0,255}$/u.test(value)) {
|
|
1128
|
+
return value;
|
|
1129
|
+
}
|
|
1130
|
+
return stableDiagnostic('method', value);
|
|
1131
|
+
}
|
|
1132
|
+
function redact(value, depth, state, key) {
|
|
1133
|
+
state.nodes += 1;
|
|
1134
|
+
if (state.nodes > maximumRawNodes || depth >= maximumRawDepth) {
|
|
1135
|
+
return '[truncated]';
|
|
1136
|
+
}
|
|
1137
|
+
if (value === null)
|
|
1138
|
+
return value;
|
|
1139
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
1140
|
+
return '[redacted]';
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof value === 'string') {
|
|
1143
|
+
return key !== undefined && structuralStringKeys.has(key)
|
|
1144
|
+
? publicStructuralValue(value)
|
|
1145
|
+
: '[redacted]';
|
|
1146
|
+
}
|
|
1147
|
+
if (Array.isArray(value)) {
|
|
1148
|
+
const output = value
|
|
1149
|
+
.slice(0, maximumRawItems)
|
|
1150
|
+
.map((item) => redact(item, depth + 1, state));
|
|
1151
|
+
if (value.length > maximumRawItems)
|
|
1152
|
+
output.push('[truncated]');
|
|
1153
|
+
return output;
|
|
1154
|
+
}
|
|
1155
|
+
if (!isRecord(value))
|
|
1156
|
+
return '[redacted]';
|
|
1157
|
+
const output = {};
|
|
1158
|
+
const entries = Object.entries(value);
|
|
1159
|
+
for (const [index, [entryKey, item]] of entries
|
|
1160
|
+
.slice(0, maximumRawItems)
|
|
1161
|
+
.entries()) {
|
|
1162
|
+
const safeKey = structuralStringKeys.has(entryKey)
|
|
1163
|
+
? entryKey
|
|
1164
|
+
: `[redacted-key-${String(index)}]`;
|
|
1165
|
+
output[safeKey] = redact(item, depth + 1, state, entryKey);
|
|
1166
|
+
}
|
|
1167
|
+
if (entries.length > maximumRawItems || state.nodes > maximumRawNodes) {
|
|
1168
|
+
output['__truncated__'] = '[truncated]';
|
|
1169
|
+
}
|
|
1170
|
+
return output;
|
|
1171
|
+
}
|
|
1172
|
+
function publicStructuralValue(value) {
|
|
1173
|
+
return knownStructuralValues.has(value)
|
|
1174
|
+
? value
|
|
1175
|
+
: stableDiagnostic('value', value);
|
|
1176
|
+
}
|
|
1177
|
+
function stableDiagnostic(prefix, value) {
|
|
1178
|
+
const digest = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
1179
|
+
return `${prefix}-${digest}`;
|
|
1180
|
+
}
|
|
1181
|
+
//# sourceMappingURL=validation.js.map
|