@opengeni/tool-gateway 0.1.0-canary.0
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 +190 -0
- package/README.md +69 -0
- package/dist/catalog.d.ts +7 -0
- package/dist/declarations.d.ts +21 -0
- package/dist/errors.d.ts +32 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +663 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
- package/src/catalog.ts +57 -0
- package/src/declarations.ts +334 -0
- package/src/errors.ts +75 -0
- package/src/index.ts +525 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import Ajv, { type ValidateFunction } from "ajv";
|
|
3
|
+
import Ajv2019 from "ajv/dist/2019.js";
|
|
4
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
5
|
+
import {
|
|
6
|
+
TOOL_GATEWAY_CATALOG_VERSION,
|
|
7
|
+
ToolGatewayCallRequest,
|
|
8
|
+
ToolGatewayCatalog,
|
|
9
|
+
ToolGatewayCatalogEntry,
|
|
10
|
+
ToolGatewayResult,
|
|
11
|
+
isToolResultSpilledReceipt,
|
|
12
|
+
type ToolGatewayCaller,
|
|
13
|
+
type ToolGatewayCatalog as ToolGatewayCatalogValue,
|
|
14
|
+
type ToolGatewayCatalogEntry as ToolGatewayCatalogEntryValue,
|
|
15
|
+
type ToolGatewayIdentity,
|
|
16
|
+
type ToolGatewayResult as ToolGatewayResultValue,
|
|
17
|
+
} from "@opengeni/contracts";
|
|
18
|
+
import {
|
|
19
|
+
assertToolGatewayCatalogSize,
|
|
20
|
+
compareCanonicalStrings,
|
|
21
|
+
digestCanonicalJson,
|
|
22
|
+
digestToolGatewayCatalog,
|
|
23
|
+
} from "./catalog";
|
|
24
|
+
import {
|
|
25
|
+
ToolGatewayApprovalRequiredError,
|
|
26
|
+
ToolGatewayCatalogStaleError,
|
|
27
|
+
ToolGatewayInputValidationError,
|
|
28
|
+
ToolGatewayOutputValidationError,
|
|
29
|
+
ToolGatewayPathCollisionError,
|
|
30
|
+
ToolGatewayToolNotFoundError,
|
|
31
|
+
} from "./errors";
|
|
32
|
+
|
|
33
|
+
export type ToolGatewayExecutionContext = {
|
|
34
|
+
operationId: string;
|
|
35
|
+
caller: ToolGatewayCaller;
|
|
36
|
+
/** In-process transport metadata; never part of catalog identity or digest. */
|
|
37
|
+
transportMeta?: Record<string, unknown> | null;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ToolGatewayCallContext = Pick<ToolGatewayExecutionContext, "transportMeta" | "signal">;
|
|
42
|
+
|
|
43
|
+
export type ToolGatewayDefinition = Omit<ToolGatewayCatalogEntryValue, "codemodePath"> & {
|
|
44
|
+
/** Optional human-readable path. Unsafe/colliding segments are normalized. */
|
|
45
|
+
codemodePath?: readonly string[];
|
|
46
|
+
/** Internal authority revision bound into human approval capabilities. */
|
|
47
|
+
approvalAuthorityDigest?: string;
|
|
48
|
+
/** Connection-backed calls must have a side-effect-free provider preflight before approval. */
|
|
49
|
+
requiresProviderPreflight?: boolean;
|
|
50
|
+
/** In-process provider preflight; never enters the public catalog or its digest. */
|
|
51
|
+
preflightCall?: (input: {
|
|
52
|
+
call: ToolGatewayCall;
|
|
53
|
+
entry: ToolGatewayCatalogEntryValue;
|
|
54
|
+
context: ToolGatewayCallContext;
|
|
55
|
+
}) => Promise<void> | void;
|
|
56
|
+
/** In-process execution lifecycle; never enters the public catalog or its digest. */
|
|
57
|
+
lifecycle?: ToolGatewayCallLifecycle;
|
|
58
|
+
execute: (
|
|
59
|
+
args: Record<string, unknown>,
|
|
60
|
+
context: ToolGatewayExecutionContext,
|
|
61
|
+
) => Promise<ToolGatewayResultValue> | ToolGatewayResultValue;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ToolGatewayAuthorization = (input: {
|
|
65
|
+
call: ToolGatewayCall;
|
|
66
|
+
entry: ToolGatewayCatalogEntryValue;
|
|
67
|
+
}) => Promise<void> | void;
|
|
68
|
+
|
|
69
|
+
export type ToolGatewayCall = {
|
|
70
|
+
operationId: string;
|
|
71
|
+
catalogDigest: string;
|
|
72
|
+
identity: ToolGatewayIdentity;
|
|
73
|
+
arguments: Record<string, unknown>;
|
|
74
|
+
caller: ToolGatewayCaller;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type ToolGatewayCallSettlement =
|
|
78
|
+
| { outcome: "completed"; result: ToolGatewayResultValue }
|
|
79
|
+
| { outcome: "failed"; error: unknown };
|
|
80
|
+
|
|
81
|
+
export type PreparedToolGatewayCallLifecycle = {
|
|
82
|
+
/** Cross the side-effect boundary immediately before the executor closure. */
|
|
83
|
+
begin?: () => Promise<void> | void;
|
|
84
|
+
/** Settle the side-effect lifecycle after a returned result or thrown failure. */
|
|
85
|
+
complete?: (settlement: ToolGatewayCallSettlement) => Promise<void> | void;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ToolGatewayCallLifecycle = {
|
|
89
|
+
/** Argument-sensitive preflight. Throwing here guarantees the executor never runs. */
|
|
90
|
+
prepare: (input: {
|
|
91
|
+
call: ToolGatewayCall;
|
|
92
|
+
entry: ToolGatewayCatalogEntryValue;
|
|
93
|
+
context: ToolGatewayCallContext;
|
|
94
|
+
}) => Promise<PreparedToolGatewayCallLifecycle | void> | PreparedToolGatewayCallLifecycle | void;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type ModelToolGatewayCall = {
|
|
98
|
+
operationId?: string;
|
|
99
|
+
modelName: string;
|
|
100
|
+
arguments: Record<string, unknown>;
|
|
101
|
+
subjectId: string;
|
|
102
|
+
transportMeta?: Record<string, unknown> | null;
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type CompiledDefinition = {
|
|
107
|
+
entry: ToolGatewayCatalogEntryValue;
|
|
108
|
+
execute: ToolGatewayDefinition["execute"];
|
|
109
|
+
approvalAuthorityDigest: string | undefined;
|
|
110
|
+
preflightCall: ToolGatewayDefinition["preflightCall"];
|
|
111
|
+
lifecycle: ToolGatewayCallLifecycle | undefined;
|
|
112
|
+
validateInput: ValidateFunction<unknown>;
|
|
113
|
+
validateOutput: ValidateFunction<unknown> | null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type PreparedToolGatewayCall = {
|
|
117
|
+
readonly call: ToolGatewayCall;
|
|
118
|
+
readonly entry: ToolGatewayCatalogEntryValue;
|
|
119
|
+
readonly approvalAuthorityDigest: string;
|
|
120
|
+
execute: () => Promise<ToolGatewayResultValue>;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export class PreparedToolGatewayDefinitions {
|
|
124
|
+
readonly entries: readonly ToolGatewayCatalogEntryValue[];
|
|
125
|
+
|
|
126
|
+
constructor(private readonly definitions: readonly CompiledDefinition[]) {
|
|
127
|
+
this.entries = definitions.map(({ entry }) => entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
create(input: {
|
|
131
|
+
catalogDigest: string;
|
|
132
|
+
authorize?: ToolGatewayAuthorization;
|
|
133
|
+
requireApproval?: (
|
|
134
|
+
entry: ToolGatewayCatalogEntryValue,
|
|
135
|
+
caller: ToolGatewayCaller,
|
|
136
|
+
context: { transportMeta?: Record<string, unknown> | null },
|
|
137
|
+
) => boolean;
|
|
138
|
+
confirmModelApproval?: (input: {
|
|
139
|
+
entry: ToolGatewayCatalogEntryValue;
|
|
140
|
+
modelName: string;
|
|
141
|
+
subjectId: string;
|
|
142
|
+
}) => boolean;
|
|
143
|
+
}): ToolGateway {
|
|
144
|
+
return new ToolGateway(
|
|
145
|
+
input.catalogDigest,
|
|
146
|
+
this.definitions,
|
|
147
|
+
input.authorize,
|
|
148
|
+
input.requireApproval,
|
|
149
|
+
input.confirmModelApproval,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export class ToolGateway {
|
|
155
|
+
private readonly byIdentity = new Map<string, CompiledDefinition>();
|
|
156
|
+
private readonly byModelName = new Map<string, CompiledDefinition>();
|
|
157
|
+
|
|
158
|
+
constructor(
|
|
159
|
+
readonly catalogDigest: string,
|
|
160
|
+
definitions: readonly CompiledDefinition[],
|
|
161
|
+
private readonly authorize: ToolGatewayAuthorization | undefined,
|
|
162
|
+
private readonly requireApproval:
|
|
163
|
+
| ((
|
|
164
|
+
entry: ToolGatewayCatalogEntryValue,
|
|
165
|
+
caller: ToolGatewayCaller,
|
|
166
|
+
context: { transportMeta?: Record<string, unknown> | null },
|
|
167
|
+
) => boolean)
|
|
168
|
+
| undefined,
|
|
169
|
+
private readonly confirmModelApproval:
|
|
170
|
+
| ((input: {
|
|
171
|
+
entry: ToolGatewayCatalogEntryValue;
|
|
172
|
+
modelName: string;
|
|
173
|
+
subjectId: string;
|
|
174
|
+
}) => boolean)
|
|
175
|
+
| undefined,
|
|
176
|
+
) {
|
|
177
|
+
for (const definition of definitions) {
|
|
178
|
+
this.byIdentity.set(identityKey(definition.entry.identity), definition);
|
|
179
|
+
this.byModelName.set(definition.entry.modelName, definition);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async call(
|
|
184
|
+
input: ToolGatewayCall,
|
|
185
|
+
context: ToolGatewayCallContext = {},
|
|
186
|
+
): Promise<ToolGatewayResultValue> {
|
|
187
|
+
return await (await this.prepareCallWithModelApproval(input, context, false)).execute();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async prepareCall(
|
|
191
|
+
input: ToolGatewayCall,
|
|
192
|
+
context: ToolGatewayCallContext = {},
|
|
193
|
+
): Promise<PreparedToolGatewayCall> {
|
|
194
|
+
return await this.prepareCallWithModelApproval(input, context, false);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private async prepareCallWithModelApproval(
|
|
198
|
+
input: ToolGatewayCall,
|
|
199
|
+
context: ToolGatewayCallContext,
|
|
200
|
+
modelApprovalConfirmed: boolean,
|
|
201
|
+
): Promise<PreparedToolGatewayCall> {
|
|
202
|
+
const request = ToolGatewayCallRequest.parse({
|
|
203
|
+
operationId: input.operationId,
|
|
204
|
+
catalogDigest: input.catalogDigest,
|
|
205
|
+
identity: input.identity,
|
|
206
|
+
arguments: input.arguments,
|
|
207
|
+
});
|
|
208
|
+
const operationId = request.operationId ?? input.operationId;
|
|
209
|
+
const caller = input.caller;
|
|
210
|
+
if (request.catalogDigest !== this.catalogDigest) {
|
|
211
|
+
throw new ToolGatewayCatalogStaleError();
|
|
212
|
+
}
|
|
213
|
+
const definition = this.byIdentity.get(identityKey(request.identity));
|
|
214
|
+
if (!definition) {
|
|
215
|
+
throw new ToolGatewayToolNotFoundError();
|
|
216
|
+
}
|
|
217
|
+
if (this.requireApproval?.(definition.entry, caller, context)) {
|
|
218
|
+
throw new ToolGatewayApprovalRequiredError();
|
|
219
|
+
}
|
|
220
|
+
if (!definition.validateInput(request.arguments)) {
|
|
221
|
+
throw new ToolGatewayInputValidationError();
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
caller.kind === "model" &&
|
|
225
|
+
definition.entry.approval === "human" &&
|
|
226
|
+
!modelApprovalConfirmed
|
|
227
|
+
) {
|
|
228
|
+
throw new ToolGatewayApprovalRequiredError();
|
|
229
|
+
}
|
|
230
|
+
const call = {
|
|
231
|
+
operationId,
|
|
232
|
+
catalogDigest: request.catalogDigest,
|
|
233
|
+
identity: request.identity,
|
|
234
|
+
arguments: request.arguments,
|
|
235
|
+
caller,
|
|
236
|
+
} satisfies ToolGatewayCall;
|
|
237
|
+
await this.authorize?.({ call, entry: definition.entry });
|
|
238
|
+
if (definition.entry.approval === "human") {
|
|
239
|
+
await definition.preflightCall?.({
|
|
240
|
+
call,
|
|
241
|
+
entry: definition.entry,
|
|
242
|
+
context,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const lifecycle = await definition.lifecycle?.prepare({
|
|
246
|
+
call,
|
|
247
|
+
entry: definition.entry,
|
|
248
|
+
context,
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
call,
|
|
252
|
+
entry: definition.entry,
|
|
253
|
+
approvalAuthorityDigest:
|
|
254
|
+
definition.approvalAuthorityDigest ??
|
|
255
|
+
digestCanonicalJson({
|
|
256
|
+
version: 1,
|
|
257
|
+
catalogDigest: this.catalogDigest,
|
|
258
|
+
identity: definition.entry.identity,
|
|
259
|
+
}),
|
|
260
|
+
execute: async () => {
|
|
261
|
+
await lifecycle?.begin?.();
|
|
262
|
+
let result: ToolGatewayResultValue;
|
|
263
|
+
try {
|
|
264
|
+
result = ToolGatewayResult.parse(
|
|
265
|
+
await definition.execute(request.arguments, {
|
|
266
|
+
operationId,
|
|
267
|
+
caller,
|
|
268
|
+
...(context.transportMeta === undefined
|
|
269
|
+
? {}
|
|
270
|
+
: { transportMeta: context.transportMeta }),
|
|
271
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
if (!result.isError && definition.validateOutput) {
|
|
275
|
+
const outputMatchesSchema =
|
|
276
|
+
result.structuredContent !== undefined &&
|
|
277
|
+
definition.validateOutput(result.structuredContent);
|
|
278
|
+
if (!outputMatchesSchema && !isToolResultSpilledReceipt(result.structuredContent)) {
|
|
279
|
+
throw new ToolGatewayOutputValidationError();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
await lifecycle?.complete?.({ outcome: "failed", error });
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
await lifecycle?.complete?.({ outcome: "completed", result });
|
|
287
|
+
return result;
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async callModel(input: ModelToolGatewayCall): Promise<ToolGatewayResultValue> {
|
|
293
|
+
const definition = this.byModelName.get(input.modelName);
|
|
294
|
+
if (!definition) {
|
|
295
|
+
throw new ToolGatewayToolNotFoundError();
|
|
296
|
+
}
|
|
297
|
+
const call = {
|
|
298
|
+
operationId: input.operationId ?? randomUUID(),
|
|
299
|
+
catalogDigest: this.catalogDigest,
|
|
300
|
+
identity: definition.entry.identity,
|
|
301
|
+
arguments: input.arguments,
|
|
302
|
+
caller: { kind: "model" as const, subjectId: input.subjectId },
|
|
303
|
+
};
|
|
304
|
+
const context = {
|
|
305
|
+
...(input.transportMeta === undefined ? {} : { transportMeta: input.transportMeta }),
|
|
306
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
307
|
+
};
|
|
308
|
+
const modelApprovalConfirmed =
|
|
309
|
+
definition.entry.approval === "human" &&
|
|
310
|
+
this.confirmModelApproval?.({
|
|
311
|
+
entry: definition.entry,
|
|
312
|
+
modelName: input.modelName,
|
|
313
|
+
subjectId: input.subjectId,
|
|
314
|
+
}) === true;
|
|
315
|
+
return await (
|
|
316
|
+
await this.prepareCallWithModelApproval(call, context, modelApprovalConfirmed)
|
|
317
|
+
).execute();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function prepareToolGatewayDefinitions(
|
|
322
|
+
definitions: readonly ToolGatewayDefinition[],
|
|
323
|
+
): PreparedToolGatewayDefinitions {
|
|
324
|
+
const paths = allocateToolPaths(definitions);
|
|
325
|
+
const schemaValidators = createSchemaValidators();
|
|
326
|
+
const compiled = definitions.map((definition, index): CompiledDefinition => {
|
|
327
|
+
const {
|
|
328
|
+
execute,
|
|
329
|
+
approvalAuthorityDigest,
|
|
330
|
+
requiresProviderPreflight: _requiresProviderPreflight,
|
|
331
|
+
preflightCall,
|
|
332
|
+
lifecycle,
|
|
333
|
+
codemodePath: _path,
|
|
334
|
+
...entryInput
|
|
335
|
+
} = definition;
|
|
336
|
+
if (approvalAuthorityDigest !== undefined && !/^[0-9a-f]{64}$/u.test(approvalAuthorityDigest)) {
|
|
337
|
+
throw new Error("Tool gateway approval authority digest must be lowercase SHA-256 hex");
|
|
338
|
+
}
|
|
339
|
+
const entry = ToolGatewayCatalogEntry.parse({
|
|
340
|
+
...entryInput,
|
|
341
|
+
codemodePath: paths[index],
|
|
342
|
+
});
|
|
343
|
+
return {
|
|
344
|
+
entry,
|
|
345
|
+
execute,
|
|
346
|
+
approvalAuthorityDigest,
|
|
347
|
+
preflightCall,
|
|
348
|
+
lifecycle,
|
|
349
|
+
validateInput: compileCatalogSchema(schemaValidators, entry.inputSchema),
|
|
350
|
+
validateOutput: entry.outputSchema
|
|
351
|
+
? compileCatalogSchema(schemaValidators, entry.outputSchema)
|
|
352
|
+
: null,
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
return new PreparedToolGatewayDefinitions(compiled);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function createWorkspaceToolGateway(input: {
|
|
359
|
+
accountId: string;
|
|
360
|
+
workspaceId: string;
|
|
361
|
+
generation: number;
|
|
362
|
+
definitions: readonly ToolGatewayDefinition[];
|
|
363
|
+
createdAt?: Date;
|
|
364
|
+
authorize?: ToolGatewayAuthorization;
|
|
365
|
+
requireApproval?: (
|
|
366
|
+
entry: ToolGatewayCatalogEntryValue,
|
|
367
|
+
caller: ToolGatewayCaller,
|
|
368
|
+
context: { transportMeta?: Record<string, unknown> | null },
|
|
369
|
+
) => boolean;
|
|
370
|
+
}): { catalog: ToolGatewayCatalogValue; gateway: ToolGateway } {
|
|
371
|
+
const prepared = prepareToolGatewayDefinitions(input.definitions);
|
|
372
|
+
const unsigned = {
|
|
373
|
+
version: TOOL_GATEWAY_CATALOG_VERSION,
|
|
374
|
+
accountId: input.accountId,
|
|
375
|
+
workspaceId: input.workspaceId,
|
|
376
|
+
generation: input.generation,
|
|
377
|
+
createdAt: (input.createdAt ?? new Date()).toISOString(),
|
|
378
|
+
entries: [...prepared.entries],
|
|
379
|
+
};
|
|
380
|
+
const catalog = ToolGatewayCatalog.parse({
|
|
381
|
+
...unsigned,
|
|
382
|
+
digest: digestToolGatewayCatalog(unsigned),
|
|
383
|
+
});
|
|
384
|
+
assertToolGatewayCatalogSize(catalog);
|
|
385
|
+
return {
|
|
386
|
+
catalog,
|
|
387
|
+
gateway: prepared.create({
|
|
388
|
+
catalogDigest: catalog.digest,
|
|
389
|
+
...(input.authorize ? { authorize: input.authorize } : {}),
|
|
390
|
+
...(input.requireApproval ? { requireApproval: input.requireApproval } : {}),
|
|
391
|
+
}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
type SchemaCompiler = { compile(schema: object): ValidateFunction<unknown> };
|
|
396
|
+
|
|
397
|
+
const COMPILED_CATALOG_SCHEMA_CACHE_MAX_ENTRIES = 512;
|
|
398
|
+
const compiledCatalogSchemaCache = new Map<string, ValidateFunction<unknown>>();
|
|
399
|
+
|
|
400
|
+
function createSchemaValidators(): {
|
|
401
|
+
draft7: SchemaCompiler;
|
|
402
|
+
draft2019: SchemaCompiler;
|
|
403
|
+
draft2020: SchemaCompiler;
|
|
404
|
+
} {
|
|
405
|
+
const options = {
|
|
406
|
+
allErrors: false,
|
|
407
|
+
coerceTypes: false,
|
|
408
|
+
strict: false,
|
|
409
|
+
useDefaults: false,
|
|
410
|
+
validateFormats: false,
|
|
411
|
+
} as const;
|
|
412
|
+
return {
|
|
413
|
+
draft7: new Ajv(options),
|
|
414
|
+
draft2019: new Ajv2019(options),
|
|
415
|
+
draft2020: new Ajv2020(options),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function compileCatalogSchema(
|
|
420
|
+
validators: ReturnType<typeof createSchemaValidators>,
|
|
421
|
+
schema: ToolGatewayCatalogEntryValue["inputSchema"],
|
|
422
|
+
): ValidateFunction<unknown> {
|
|
423
|
+
const dialect = typeof schema.$schema === "string" ? schema.$schema : "";
|
|
424
|
+
const family = dialect.includes("2020-12")
|
|
425
|
+
? "2020-12"
|
|
426
|
+
: dialect.includes("2019-09")
|
|
427
|
+
? "2019-09"
|
|
428
|
+
: "draft7";
|
|
429
|
+
const cacheKey = `${family}:${digestCanonicalJson(schema)}`;
|
|
430
|
+
const cached = compiledCatalogSchemaCache.get(cacheKey);
|
|
431
|
+
if (cached) {
|
|
432
|
+
compiledCatalogSchemaCache.delete(cacheKey);
|
|
433
|
+
compiledCatalogSchemaCache.set(cacheKey, cached);
|
|
434
|
+
return cached;
|
|
435
|
+
}
|
|
436
|
+
const compiled =
|
|
437
|
+
family === "2020-12"
|
|
438
|
+
? validators.draft2020.compile(schema)
|
|
439
|
+
: family === "2019-09"
|
|
440
|
+
? validators.draft2019.compile(schema)
|
|
441
|
+
: validators.draft7.compile(schema);
|
|
442
|
+
while (compiledCatalogSchemaCache.size >= COMPILED_CATALOG_SCHEMA_CACHE_MAX_ENTRIES) {
|
|
443
|
+
const oldest = compiledCatalogSchemaCache.keys().next().value;
|
|
444
|
+
if (oldest === undefined) break;
|
|
445
|
+
compiledCatalogSchemaCache.delete(oldest);
|
|
446
|
+
}
|
|
447
|
+
compiledCatalogSchemaCache.set(cacheKey, compiled);
|
|
448
|
+
return compiled;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function allocateToolPaths(definitions: readonly ToolGatewayDefinition[]): string[][] {
|
|
452
|
+
const requested = definitions.map((definition) =>
|
|
453
|
+
definition.codemodePath?.length
|
|
454
|
+
? [...definition.codemodePath]
|
|
455
|
+
: [definition.identity.serverId, definition.identity.toolName],
|
|
456
|
+
);
|
|
457
|
+
const bases = requested.map((path) => path.map(safeNamespaceSegment));
|
|
458
|
+
const allocated = bases.map((base, index) => {
|
|
459
|
+
const path = requested[index]!;
|
|
460
|
+
if (path.every((segment, segmentIndex) => segment === base[segmentIndex])) return base;
|
|
461
|
+
const suffix = `_${shortIdentityDigest(definitions[index]!.identity)}`;
|
|
462
|
+
const last = base.at(-1)!;
|
|
463
|
+
return [...base.slice(0, -1), `${last.slice(0, 128 - suffix.length)}${suffix}`];
|
|
464
|
+
});
|
|
465
|
+
assertNoToolPathCollisions(allocated);
|
|
466
|
+
return allocated;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
type ToolPathNode = {
|
|
470
|
+
children: Map<string, ToolPathNode>;
|
|
471
|
+
leaf: boolean;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
function assertNoToolPathCollisions(paths: readonly (readonly string[])[]): void {
|
|
475
|
+
const root: ToolPathNode = { children: new Map(), leaf: false };
|
|
476
|
+
const ordered = [...paths].sort(compareToolPaths);
|
|
477
|
+
for (const path of ordered) {
|
|
478
|
+
let node = root;
|
|
479
|
+
for (const [index, segment] of path.entries()) {
|
|
480
|
+
if (node.leaf) throw new ToolGatewayPathCollisionError(path, "extends_leaf");
|
|
481
|
+
let child = node.children.get(segment);
|
|
482
|
+
if (!child) {
|
|
483
|
+
child = { children: new Map(), leaf: false };
|
|
484
|
+
node.children.set(segment, child);
|
|
485
|
+
}
|
|
486
|
+
node = child;
|
|
487
|
+
if (index === path.length - 1) {
|
|
488
|
+
if (node.leaf || node.children.size > 0) {
|
|
489
|
+
throw new ToolGatewayPathCollisionError(path, "collision");
|
|
490
|
+
}
|
|
491
|
+
node.leaf = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function compareToolPaths(left: readonly string[], right: readonly string[]): number {
|
|
498
|
+
const length = Math.min(left.length, right.length);
|
|
499
|
+
for (let index = 0; index < length; index += 1) {
|
|
500
|
+
const compared = compareCanonicalStrings(left[index]!, right[index]!);
|
|
501
|
+
if (compared !== 0) return compared;
|
|
502
|
+
}
|
|
503
|
+
return left.length - right.length;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function safeNamespaceSegment(value: string): string {
|
|
507
|
+
let normalized = value.replace(/[^A-Za-z0-9_$]/gu, "_");
|
|
508
|
+
if (!/^[A-Za-z_$]/u.test(normalized)) normalized = `_${normalized}`;
|
|
509
|
+
if (["__proto__", "prototype", "constructor"].includes(normalized)) {
|
|
510
|
+
normalized = `_${normalized}`;
|
|
511
|
+
}
|
|
512
|
+
return normalized.slice(0, 128) || "_";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function shortIdentityDigest(identity: ToolGatewayIdentity): string {
|
|
516
|
+
return createHash("sha256").update(identityKey(identity), "utf8").digest("hex").slice(0, 10);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function identityKey(identity: ToolGatewayIdentity): string {
|
|
520
|
+
return `${identity.serverId}\u0000${identity.toolName}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export * from "./catalog";
|
|
524
|
+
export * from "./declarations";
|
|
525
|
+
export * from "./errors";
|