@zackbart/connecta 0.24.1 → 0.24.3
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/CHANGELOG.md +169 -0
- package/dist/auth/bearer.js +2 -0
- package/dist/auth/clerk.d.ts +0 -5
- package/dist/auth/clerk.js +21 -8
- package/dist/auth/downstream-oauth.d.ts +12 -1
- package/dist/auth/downstream-oauth.js +147 -35
- package/dist/call-admission.d.ts +4 -0
- package/dist/call-admission.js +26 -0
- package/dist/catalog-drift.js +9 -4
- package/dist/catalog-service.d.ts +2 -0
- package/dist/catalog-service.js +25 -8
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +246 -121
- package/dist/connector-access.d.ts +32 -0
- package/dist/connector-access.js +79 -0
- package/dist/connectors/api.js +11 -1
- package/dist/connectors/guarded-fetch.d.ts +1 -1
- package/dist/connectors/guarded-fetch.js +27 -20
- package/dist/connectors/remote-mcp.js +84 -53
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +58 -0
- package/dist/execute.js +85 -23
- package/dist/executor-result.js +3 -1
- package/dist/executors/quickjs-child.js +5 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -0
- package/dist/executors/quickjs-runtime.d.ts +1 -1
- package/dist/executors/quickjs-runtime.js +38 -21
- package/dist/executors/quickjs.js +68 -27
- package/dist/index.d.ts +37 -1
- package/dist/index.js +89 -3
- package/dist/invocation.js +134 -93
- package/dist/mcp-result.js +3 -2
- package/dist/meta-tools.js +118 -39
- package/dist/registry.d.ts +29 -1
- package/dist/registry.js +122 -15
- package/dist/routes/credentials.js +1 -0
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +112 -12
- package/dist/routes/oauth-management.js +1 -0
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +7 -1
- package/dist/routes/shared.js +12 -13
- package/dist/routes/ui.js +2 -1
- package/dist/server.js +15 -3
- package/dist/skills.js +6 -5
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.js +312 -34
- package/dist/storage/memory.js +12 -1
- package/dist/validate.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +30 -9
- package/documentation/auth.md +110 -6
- package/documentation/call-admission.md +24 -8
- package/documentation/code-mode.md +34 -22
- package/documentation/connectors.md +47 -5
- package/documentation/meta-tools.md +74 -6
- package/documentation/operations.md +20 -19
- package/documentation/provider-conventions.md +7 -0
- package/documentation/request-admission.md +38 -4
- package/documentation/storage-and-credentials.md +54 -1
- package/documentation/upgrading.md +21 -5
- package/ethos.md +1 -1
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
|
@@ -114,6 +114,8 @@ export interface CatalogDescription {
|
|
|
114
114
|
inputSchema?: unknown;
|
|
115
115
|
outputSchema?: unknown;
|
|
116
116
|
outputSchemaSource?: "observed";
|
|
117
|
+
inputSchemaTruncated?: true;
|
|
118
|
+
outputSchemaTruncated?: true;
|
|
117
119
|
annotations?: ToolDef["annotations"];
|
|
118
120
|
error?: string;
|
|
119
121
|
errorDetails?: CatalogDescriptionFailureDetail;
|
package/dist/catalog-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compactDiscoverySchema,
|
|
1
|
+
import { compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
|
|
2
2
|
import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
|
|
3
3
|
import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
|
|
4
4
|
import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
|
|
@@ -180,9 +180,6 @@ function schemaKeyMetadata(input, output) {
|
|
|
180
180
|
: {}),
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
-
function renderSchema(schema, format) {
|
|
184
|
-
return format === "json" ? schema : compactSchema(schema);
|
|
185
|
-
}
|
|
186
183
|
function renderSearchSchema(schema, format) {
|
|
187
184
|
if (format === "json")
|
|
188
185
|
return { schema, truncated: false };
|
|
@@ -337,11 +334,25 @@ export class CatalogService {
|
|
|
337
334
|
};
|
|
338
335
|
}
|
|
339
336
|
async search(args) {
|
|
337
|
+
if (args.query !== undefined && typeof args.query !== "string") {
|
|
338
|
+
throw new DiscoveryPolicyError("invalid_args", "query must be a string. Omit it or use an empty string to browse the catalog.");
|
|
339
|
+
}
|
|
340
|
+
if (args.connector !== undefined &&
|
|
341
|
+
boundedEchoText(args.connector) !== args.connector) {
|
|
342
|
+
// The scope is echoed back as `queryAnalysis.connectorScope`; a clipped
|
|
343
|
+
// copy could name a different connector, so refuse instead of clamping.
|
|
344
|
+
throw new DiscoveryPolicyError("invalid_args", "connector must be at most 512 UTF-8 bytes.");
|
|
345
|
+
}
|
|
340
346
|
const query = args.query ?? "";
|
|
341
347
|
const retrievalQuery = lexicalSearchQuery(query);
|
|
342
348
|
const safety = discoverySafety(args.safety);
|
|
343
349
|
const limit = discoverySearchLimit(args.limit);
|
|
344
|
-
|
|
350
|
+
if (args.offset !== undefined && (typeof args.offset !== "number" ||
|
|
351
|
+
!Number.isInteger(args.offset) ||
|
|
352
|
+
args.offset < 0)) {
|
|
353
|
+
throw new DiscoveryPolicyError("invalid_args", "offset must be a non-negative whole number. Start at 0 or use the previous page's nextOffset.");
|
|
354
|
+
}
|
|
355
|
+
const offset = args.offset ?? 0;
|
|
345
356
|
const scopedConnector = args.connector
|
|
346
357
|
? this.registry.getConnector(args.connector)
|
|
347
358
|
: undefined;
|
|
@@ -768,7 +779,11 @@ export class CatalogService {
|
|
|
768
779
|
const input = tool.inputSchema ?? { type: "object" };
|
|
769
780
|
const output = this.outputSchema(addressResolution.connector.id, tool);
|
|
770
781
|
const description = summarizeDescription(tool.description, args.fullDescriptions === true);
|
|
771
|
-
const
|
|
782
|
+
const compactInput = format === "compact"
|
|
783
|
+
? compactDescriptionSchema(input) : undefined;
|
|
784
|
+
const compactOutput = format === "compact" && output.schema
|
|
785
|
+
? compactDescriptionSchema(output.schema) : undefined;
|
|
786
|
+
const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, compactInput?.truncated === true || compactOutput?.truncated === true);
|
|
772
787
|
const guideSummary = connectorGuideSummary(addressResolution.connector);
|
|
773
788
|
return {
|
|
774
789
|
address,
|
|
@@ -786,10 +801,12 @@ export class CatalogService {
|
|
|
786
801
|
guideRequiredReasons: requiredReasons,
|
|
787
802
|
}
|
|
788
803
|
: {}),
|
|
789
|
-
inputSchema:
|
|
804
|
+
inputSchema: compactInput?.text ?? input,
|
|
805
|
+
...(compactInput?.truncated ? { inputSchemaTruncated: true } : {}),
|
|
806
|
+
...(compactOutput?.truncated ? { outputSchemaTruncated: true } : {}),
|
|
790
807
|
...(output.schema
|
|
791
808
|
? {
|
|
792
|
-
outputSchema:
|
|
809
|
+
outputSchema: compactOutput?.text ?? output.schema,
|
|
793
810
|
}
|
|
794
811
|
: {}),
|
|
795
812
|
...(output.source ? { outputSchemaSource: output.source } : {}),
|
package/dist/catalog.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export declare function lexicalCorpusStatistics(toolSets: ToolDef[][], query: st
|
|
|
50
50
|
export declare function rankTools(tools: ToolDef[], query: string, mode?: LexicalMatchMode, statistics?: LexicalCorpusStatistics, exactNameQuery?: string): RankedTool[];
|
|
51
51
|
/** Render and cache a compact TypeScript-like representation of JSON Schema. */
|
|
52
52
|
export declare function compactSchema(schema: JsonSchema): string;
|
|
53
|
+
/** Describe allows 8 KiB for property prose, with the same work cap as search. */
|
|
54
|
+
export declare function compactDescriptionSchema(schema: JsonSchema): CompactDiscoverySchema;
|
|
53
55
|
export interface CompactDiscoverySchema {
|
|
54
56
|
text: string;
|
|
55
57
|
truncated: boolean;
|
package/dist/catalog.js
CHANGED
|
@@ -5,6 +5,53 @@ const MAX_COMPACT_DISCOVERY_ENUM_BYTES = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES / 4;
|
|
|
5
5
|
const MAX_COMPACT_DISCOVERY_CONSTRAINT_BYTES = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES / 4;
|
|
6
6
|
const schemaEncoder = new TextEncoder();
|
|
7
7
|
const COMPACT_DISCOVERY_TRUNCATION = " /* truncated */";
|
|
8
|
+
const MAX_COMPACT_DESCRIPTION_SCHEMA_BYTES = 8_192;
|
|
9
|
+
const MAX_SCHEMA_WORK = 2_000;
|
|
10
|
+
const schemaWorkExceeded = Symbol("schema work budget exceeded");
|
|
11
|
+
const schemaSizeExceeded = Symbol("schema byte budget exceeded");
|
|
12
|
+
class SchemaWork {
|
|
13
|
+
byteLimit;
|
|
14
|
+
remaining = MAX_SCHEMA_WORK;
|
|
15
|
+
truncated = false;
|
|
16
|
+
refs = new Map();
|
|
17
|
+
constructor(byteLimit = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES) {
|
|
18
|
+
this.byteLimit = byteLimit;
|
|
19
|
+
}
|
|
20
|
+
visit() {
|
|
21
|
+
if (this.remaining-- <= 0)
|
|
22
|
+
throw schemaWorkExceeded;
|
|
23
|
+
}
|
|
24
|
+
text(value) {
|
|
25
|
+
// Check code units first so encoding a hostile scalar is itself bounded.
|
|
26
|
+
if (value.length > this.byteLimit ||
|
|
27
|
+
schemaEncoder.encode(value).length > this.byteLimit) {
|
|
28
|
+
throw schemaSizeExceeded;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
json(value) {
|
|
33
|
+
// The raw-JSON fallback and const/enum values must spend the same work
|
|
34
|
+
// budget as schema nodes, including values nested inside unknown keywords.
|
|
35
|
+
const visit = this.visit.bind(this);
|
|
36
|
+
const text = this.text.bind(this);
|
|
37
|
+
const ancestors = [];
|
|
38
|
+
return this.text(JSON.stringify(value, function (key, item) {
|
|
39
|
+
visit();
|
|
40
|
+
text(key);
|
|
41
|
+
if (typeof item === "string")
|
|
42
|
+
text(item);
|
|
43
|
+
if (item !== null && typeof item === "object") {
|
|
44
|
+
while (ancestors.length && ancestors[ancestors.length - 1] !== this) {
|
|
45
|
+
ancestors.pop();
|
|
46
|
+
}
|
|
47
|
+
if (ancestors.length > 32)
|
|
48
|
+
throw schemaWorkExceeded;
|
|
49
|
+
ancestors.push(item);
|
|
50
|
+
}
|
|
51
|
+
return item;
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
8
55
|
export function summarizeDescription(text, full) {
|
|
9
56
|
return summarizeToLength(text, full, DEFAULT_DESCRIPTION_LENGTH);
|
|
10
57
|
}
|
|
@@ -315,6 +362,13 @@ function refName(ref) {
|
|
|
315
362
|
*/
|
|
316
363
|
function declaresShape(s) {
|
|
317
364
|
return (typeof s.$ref === "string" ||
|
|
365
|
+
typeof s.$dynamicRef === "string" ||
|
|
366
|
+
Array.isArray(s.allOf) ||
|
|
367
|
+
Array.isArray(s.prefixItems) ||
|
|
368
|
+
s.dependentSchemas !== undefined ||
|
|
369
|
+
s.if !== undefined ||
|
|
370
|
+
s.then !== undefined ||
|
|
371
|
+
s.else !== undefined ||
|
|
318
372
|
Array.isArray(s.oneOf) ||
|
|
319
373
|
Array.isArray(s.anyOf) ||
|
|
320
374
|
Array.isArray(s.enum) ||
|
|
@@ -342,30 +396,42 @@ function grouped(part) {
|
|
|
342
396
|
}
|
|
343
397
|
return part;
|
|
344
398
|
}
|
|
345
|
-
function renderEnum(values, byteLimit, onTruncated) {
|
|
399
|
+
function renderEnum(values, work, byteLimit, onTruncated) {
|
|
346
400
|
if (values.length === 0)
|
|
347
401
|
return "never";
|
|
348
|
-
const
|
|
349
|
-
const full = renderedValues.join(" | ");
|
|
350
|
-
if (byteLimit === undefined ||
|
|
351
|
-
schemaEncoder.encode(full).length <= byteLimit) {
|
|
352
|
-
return full;
|
|
353
|
-
}
|
|
354
|
-
onTruncated?.();
|
|
402
|
+
const limit = byteLimit ?? MAX_COMPACT_DISCOVERY_ENUM_BYTES;
|
|
355
403
|
const marker = (omitted) => `unknown /* ${omitted} enum ${omitted === 1 ? "value" : "values"} omitted */`;
|
|
356
404
|
let rendered = `(${marker(values.length)})`;
|
|
357
405
|
const prefix = [];
|
|
358
|
-
for (let index = 0; index <
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
406
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
407
|
+
let value;
|
|
408
|
+
try {
|
|
409
|
+
value = work.json(values[index]);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
if (error !== schemaSizeExceeded)
|
|
413
|
+
throw error;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
prefix.push(value);
|
|
417
|
+
const full = prefix.join(" | ");
|
|
418
|
+
if (schemaEncoder.encode(full).length > limit)
|
|
363
419
|
break;
|
|
364
|
-
|
|
420
|
+
if (index === values.length - 1)
|
|
421
|
+
return full;
|
|
422
|
+
const omitted = values.length - prefix.length;
|
|
423
|
+
const candidate = `(${full} | ${marker(omitted)})`;
|
|
424
|
+
if (schemaEncoder.encode(candidate).length <= limit)
|
|
425
|
+
rendered = candidate;
|
|
365
426
|
}
|
|
427
|
+
onTruncated?.();
|
|
366
428
|
return rendered;
|
|
367
429
|
}
|
|
368
430
|
function safeConstraintValue(value) {
|
|
431
|
+
if (value.length > MAX_COMPACT_DESCRIPTION_SCHEMA_BYTES) {
|
|
432
|
+
// This placeholder only participates in the byte check and is dropped whole.
|
|
433
|
+
return "x".repeat(MAX_COMPACT_DESCRIPTION_SCHEMA_BYTES + 1);
|
|
434
|
+
}
|
|
369
435
|
return JSON.stringify(value).replaceAll("*/", "*\\/");
|
|
370
436
|
}
|
|
371
437
|
function constraintEntries(schema) {
|
|
@@ -416,17 +482,36 @@ function renderConstraints(base, schema, byteLimit, onTruncated) {
|
|
|
416
482
|
: `${grouped(base)} /* ${kept.join("; ")} */`;
|
|
417
483
|
}
|
|
418
484
|
function renderSchema(schema, defs, seen, depth, options) {
|
|
485
|
+
options.work.visit();
|
|
486
|
+
return options.work.text(renderSchemaNode(schema, defs, seen, depth, options));
|
|
487
|
+
}
|
|
488
|
+
function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
419
489
|
if (depth > 4)
|
|
420
490
|
return "…";
|
|
421
491
|
if (schema === null || typeof schema !== "object") {
|
|
422
|
-
return
|
|
492
|
+
return options.work.json(schema);
|
|
423
493
|
}
|
|
424
494
|
const s = schema;
|
|
425
495
|
const constrain = (rendered) => options.renderConstraints
|
|
426
496
|
? renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated)
|
|
427
497
|
: rendered;
|
|
498
|
+
// Conditions cannot be expressed by a single static shape. Preserve the
|
|
499
|
+
// base and send callers to the exact schema instead of hiding the rules.
|
|
500
|
+
if (s.dependentSchemas !== undefined || s.if !== undefined ||
|
|
501
|
+
s.then !== undefined || s.else !== undefined) {
|
|
502
|
+
options.work.truncated = true;
|
|
503
|
+
const base = Object.create(null);
|
|
504
|
+
for (const key of propertyNames(s, options.work)) {
|
|
505
|
+
if (!["dependentSchemas", "if", "then", "else"].includes(key))
|
|
506
|
+
base[key] = s[key];
|
|
507
|
+
}
|
|
508
|
+
const rendered = declaresShape(base)
|
|
509
|
+
? renderSchema(base, defs, seen, depth, options)
|
|
510
|
+
: "unknown";
|
|
511
|
+
return `${rendered} /* conditional */`;
|
|
512
|
+
}
|
|
428
513
|
// allOf composes rather than replaces: it is checked before every other
|
|
429
|
-
// keyword, and renders the schema's own shape alongside its members instead
|
|
514
|
+
// shape keyword, and renders the schema's own shape alongside its members instead
|
|
430
515
|
// of returning early. A schema carrying both allOf and properties (the usual
|
|
431
516
|
// OpenAPI-derived "extend this base" shape, and equally legal with $ref,
|
|
432
517
|
// enum, const, or items) would otherwise silently drop whichever half lost
|
|
@@ -434,7 +519,11 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
434
519
|
// specific half, and is rendered at the current depth because its members
|
|
435
520
|
// sit at this nesting level, not one below.
|
|
436
521
|
if (Array.isArray(s.allOf)) {
|
|
437
|
-
const
|
|
522
|
+
const own = Object.create(null);
|
|
523
|
+
for (const key of propertyNames(s, options.work)) {
|
|
524
|
+
if (key !== "allOf")
|
|
525
|
+
own[key] = s[key];
|
|
526
|
+
}
|
|
438
527
|
const parts = declaresShape(own)
|
|
439
528
|
? [renderSchema(own, defs, seen, depth, options)]
|
|
440
529
|
: [];
|
|
@@ -447,16 +536,27 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
447
536
|
return parts[0];
|
|
448
537
|
return parts.map(grouped).join(" & ");
|
|
449
538
|
}
|
|
450
|
-
|
|
451
|
-
|
|
539
|
+
const reference = s.$ref ?? s.$dynamicRef;
|
|
540
|
+
if (typeof reference === "string") {
|
|
541
|
+
const dynamic = s.$ref === undefined;
|
|
542
|
+
const rawName = refName(options.work.text(reference));
|
|
543
|
+
const name = dynamic ? rawName.replace(/^#/, "") : rawName;
|
|
452
544
|
if (seen.has(name))
|
|
453
545
|
return name;
|
|
454
|
-
const target = defs
|
|
455
|
-
if (target === undefined)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
546
|
+
const target = resolveDefinition(defs, name);
|
|
547
|
+
if (target === undefined) {
|
|
548
|
+
if (dynamic)
|
|
549
|
+
options.work.truncated = true;
|
|
550
|
+
return dynamic ? "unknown" : name;
|
|
551
|
+
}
|
|
552
|
+
const cacheKey = JSON.stringify([name, depth, [...seen]]);
|
|
553
|
+
let rendered = options.work.refs.get(cacheKey);
|
|
554
|
+
if (rendered === undefined) {
|
|
555
|
+
seen.add(name);
|
|
556
|
+
rendered = renderSchema(target, defs, seen, depth, options);
|
|
557
|
+
seen.delete(name);
|
|
558
|
+
options.work.refs.set(cacheKey, rendered);
|
|
559
|
+
}
|
|
460
560
|
return constrain(rendered);
|
|
461
561
|
}
|
|
462
562
|
const union = (s.oneOf ?? s.anyOf);
|
|
@@ -468,7 +568,7 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
468
568
|
return constrain(rendered);
|
|
469
569
|
}
|
|
470
570
|
if (Array.isArray(s.enum)) {
|
|
471
|
-
const rendered = renderEnum(s.enum, options.enumByteLimit, options.onEnumTruncated);
|
|
571
|
+
const rendered = renderEnum(s.enum, options.work, options.enumByteLimit, options.onEnumTruncated);
|
|
472
572
|
return constrain(rendered);
|
|
473
573
|
}
|
|
474
574
|
// Checked before type/properties so a discriminator like
|
|
@@ -476,10 +576,20 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
476
576
|
// JSON.stringify(undefined) returns undefined (not a string), so an explicit
|
|
477
577
|
// `const: undefined` must fall through to the regular type rendering.
|
|
478
578
|
if (s.const !== undefined) {
|
|
479
|
-
const rendered =
|
|
579
|
+
const rendered = options.work.json(s.const);
|
|
480
580
|
return constrain(rendered);
|
|
481
581
|
}
|
|
482
582
|
const type = s.type;
|
|
583
|
+
if (Array.isArray(s.prefixItems)) {
|
|
584
|
+
const parts = s.prefixItems.map((item) => renderSchema(item, defs, seen, depth + 1, options));
|
|
585
|
+
if (s.items !== false) {
|
|
586
|
+
const rest = s.items === undefined || s.items === true
|
|
587
|
+
? "unknown"
|
|
588
|
+
: renderSchema(s.items, defs, seen, depth + 1, options);
|
|
589
|
+
parts.push(`...${grouped(rest)}[]`);
|
|
590
|
+
}
|
|
591
|
+
return `[${parts.join(", ")}]`;
|
|
592
|
+
}
|
|
483
593
|
if (type === "array" || s.items) {
|
|
484
594
|
const items = s.items
|
|
485
595
|
? renderSchema(s.items, defs, seen, depth + 1, options)
|
|
@@ -488,8 +598,8 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
488
598
|
}
|
|
489
599
|
if (type === "object" || s.properties) {
|
|
490
600
|
const props = (s.properties ?? {});
|
|
491
|
-
const required = new Set((
|
|
492
|
-
const declaredKeys =
|
|
601
|
+
const required = new Set(schemaRequired(s, options.work));
|
|
602
|
+
const declaredKeys = propertyNames(props, options.work);
|
|
493
603
|
const keys = options.requiredFirst
|
|
494
604
|
? [
|
|
495
605
|
...declaredKeys.filter((key) => required.has(key)),
|
|
@@ -503,6 +613,10 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
503
613
|
const optional = required.has(key) ? "" : "?";
|
|
504
614
|
const rendered = renderSchema(props[key], defs, seen, depth + 1, options);
|
|
505
615
|
const description = props[key]?.description;
|
|
616
|
+
if (options.propertyDescriptions && typeof description === "string") {
|
|
617
|
+
options.work.text(description);
|
|
618
|
+
}
|
|
619
|
+
options.work.text(key);
|
|
506
620
|
const comment = options.propertyDescriptions && typeof description === "string"
|
|
507
621
|
? ` // ${description}`
|
|
508
622
|
: "";
|
|
@@ -511,43 +625,40 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
511
625
|
.join(", ")} }`;
|
|
512
626
|
}
|
|
513
627
|
if (typeof type === "string") {
|
|
514
|
-
return constrain(type);
|
|
628
|
+
return constrain(options.work.text(type));
|
|
515
629
|
}
|
|
516
630
|
if (Array.isArray(type)) {
|
|
517
|
-
const rendered = type.
|
|
631
|
+
const rendered = type.map((item) => {
|
|
632
|
+
options.work.visit();
|
|
633
|
+
return options.work.text(String(item));
|
|
634
|
+
}).join(" | ");
|
|
518
635
|
return constrain(rendered);
|
|
519
636
|
}
|
|
520
637
|
if (options.renderConstraints && constraintEntries(s).length > 0) {
|
|
521
638
|
return renderConstraints("unknown", s, options.constraintByteLimit, options.onConstraintTruncated);
|
|
522
639
|
}
|
|
523
|
-
return
|
|
640
|
+
return options.work.json(schema);
|
|
524
641
|
}
|
|
525
642
|
const compactSchemas = new WeakMap();
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
643
|
+
function resolveDefinition(schema, name) {
|
|
644
|
+
const definitions = schema.definitions;
|
|
645
|
+
const defs = schema.$defs;
|
|
646
|
+
return definitions && Object.hasOwn(definitions, name)
|
|
647
|
+
? definitions[name]
|
|
648
|
+
: defs && Object.hasOwn(defs, name) ? defs[name] : undefined;
|
|
531
649
|
}
|
|
532
650
|
/** Render and cache a compact TypeScript-like representation of JSON Schema. */
|
|
533
651
|
export function compactSchema(schema) {
|
|
652
|
+
return compactDescriptionSchema(schema).text;
|
|
653
|
+
}
|
|
654
|
+
/** Describe allows 8 KiB for property prose, with the same work cap as search. */
|
|
655
|
+
export function compactDescriptionSchema(schema) {
|
|
534
656
|
const cached = compactSchemas.get(schema);
|
|
535
657
|
if (cached)
|
|
536
658
|
return cached;
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
rendered = renderSchema(schema, defs, new Set(), 0, {
|
|
541
|
-
propertyDescriptions: true,
|
|
542
|
-
requiredFirst: false,
|
|
543
|
-
renderConstraints: true,
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
catch {
|
|
547
|
-
rendered = JSON.stringify(schema);
|
|
548
|
-
}
|
|
549
|
-
compactSchemas.set(schema, rendered);
|
|
550
|
-
return rendered;
|
|
659
|
+
const result = boundedCompactSchema(schema, true);
|
|
660
|
+
compactSchemas.set(schema, result);
|
|
661
|
+
return result;
|
|
551
662
|
}
|
|
552
663
|
const compactDiscoverySchemas = new WeakMap();
|
|
553
664
|
/**
|
|
@@ -558,8 +669,12 @@ const compactDiscoverySchemas = new WeakMap();
|
|
|
558
669
|
* Types become `unknown`: pretending a severed nested type is exact would be
|
|
559
670
|
* worse than making the existing truncation flag's recovery route explicit.
|
|
560
671
|
*/
|
|
561
|
-
function truncatedDiscoverySchema(schema) {
|
|
562
|
-
|
|
672
|
+
function truncatedDiscoverySchema(schema, work) {
|
|
673
|
+
let keys;
|
|
674
|
+
try {
|
|
675
|
+
keys = objectKeys(schema, schema, new Set(), 0, work);
|
|
676
|
+
}
|
|
677
|
+
catch { /* An exhausted walk has no reliable key inventory. */ }
|
|
563
678
|
if (!keys)
|
|
564
679
|
return `unknown${COMPACT_DISCOVERY_TRUNCATION}`;
|
|
565
680
|
const required = new Set(keys.required);
|
|
@@ -594,63 +709,44 @@ export function compactDiscoverySchema(schema) {
|
|
|
594
709
|
const cached = compactDiscoverySchemas.get(schema);
|
|
595
710
|
if (cached)
|
|
596
711
|
return cached;
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
712
|
+
const result = boundedCompactSchema(schema, false);
|
|
713
|
+
compactDiscoverySchemas.set(schema, result);
|
|
714
|
+
return result;
|
|
715
|
+
}
|
|
716
|
+
function boundedCompactSchema(schema, description) {
|
|
717
|
+
const work = new SchemaWork(description ? MAX_COMPACT_DESCRIPTION_SCHEMA_BYTES : MAX_COMPACT_DISCOVERY_SCHEMA_BYTES);
|
|
718
|
+
const options = {
|
|
719
|
+
work,
|
|
720
|
+
propertyDescriptions: description,
|
|
721
|
+
requiredFirst: !description,
|
|
722
|
+
enumByteLimit: description ? work.byteLimit : MAX_COMPACT_DISCOVERY_ENUM_BYTES,
|
|
723
|
+
onEnumTruncated: () => { work.truncated = true; },
|
|
724
|
+
renderConstraints: true,
|
|
725
|
+
constraintByteLimit: description ? work.byteLimit : MAX_COMPACT_DISCOVERY_CONSTRAINT_BYTES,
|
|
726
|
+
onConstraintTruncated: () => { work.truncated = true; },
|
|
608
727
|
};
|
|
609
728
|
try {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
rendered = renderSchema(schema, defs, new Set(), 0, {
|
|
629
|
-
...base,
|
|
630
|
-
renderConstraints: false,
|
|
631
|
-
});
|
|
632
|
-
constraintTruncated = true;
|
|
633
|
-
}
|
|
634
|
-
catch {
|
|
635
|
-
rendered = JSON.stringify(schema);
|
|
729
|
+
const text = renderSchema(schema, schema, new Set(), 0, options);
|
|
730
|
+
return { text, truncated: work.truncated };
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
// A constraint-free retry shares the original work budget. Repeated refs
|
|
734
|
+
// are memoized only within each pass because their text includes constraints.
|
|
735
|
+
if (error === schemaSizeExceeded && !description) {
|
|
736
|
+
work.refs.clear();
|
|
737
|
+
try {
|
|
738
|
+
return {
|
|
739
|
+
text: renderSchema(schema, schema, new Set(), 0, {
|
|
740
|
+
...options,
|
|
741
|
+
renderConstraints: false,
|
|
742
|
+
}),
|
|
743
|
+
truncated: true,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
catch { /* Fall through to a bounded key-only shape. */ }
|
|
636
747
|
}
|
|
748
|
+
return { text: truncatedDiscoverySchema(schema, work), truncated: true };
|
|
637
749
|
}
|
|
638
|
-
const bytes = schemaEncoder.encode(rendered);
|
|
639
|
-
let result;
|
|
640
|
-
if (bytes.length <= MAX_COMPACT_DISCOVERY_SCHEMA_BYTES) {
|
|
641
|
-
result = {
|
|
642
|
-
text: rendered,
|
|
643
|
-
truncated: enumTruncated || constraintTruncated,
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
result = {
|
|
648
|
-
text: truncatedDiscoverySchema(schema),
|
|
649
|
-
truncated: true,
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
compactDiscoverySchemas.set(schema, result);
|
|
653
|
-
return result;
|
|
654
750
|
}
|
|
655
751
|
/**
|
|
656
752
|
* Walk a schema the way renderSchema does — composing `allOf` and resolving
|
|
@@ -669,9 +765,8 @@ export function compactDiscoverySchema(schema) {
|
|
|
669
765
|
export function schemaObjectKeys(schema) {
|
|
670
766
|
if (!schema)
|
|
671
767
|
return undefined;
|
|
672
|
-
const defs = defsOf(schema);
|
|
673
768
|
try {
|
|
674
|
-
return objectKeys(schema,
|
|
769
|
+
return objectKeys(schema, schema, new Set(), 0, new SchemaWork());
|
|
675
770
|
}
|
|
676
771
|
catch {
|
|
677
772
|
return undefined;
|
|
@@ -687,19 +782,27 @@ function mergedKeys(parts) {
|
|
|
687
782
|
};
|
|
688
783
|
}
|
|
689
784
|
/** The key-collecting twin of renderSchema; the branch order must match it. */
|
|
690
|
-
function objectKeys(schema, defs, seen, depth) {
|
|
785
|
+
function objectKeys(schema, defs, seen, depth, work) {
|
|
786
|
+
work.visit();
|
|
691
787
|
if (depth > 4)
|
|
692
788
|
return undefined;
|
|
693
789
|
if (schema === null || typeof schema !== "object")
|
|
694
790
|
return undefined;
|
|
695
791
|
const s = schema;
|
|
696
792
|
if (Array.isArray(s.allOf)) {
|
|
697
|
-
const
|
|
793
|
+
const own = Object.create(null);
|
|
794
|
+
for (const key of propertyNames(s, work)) {
|
|
795
|
+
if (key !== "allOf")
|
|
796
|
+
own[key] = s[key];
|
|
797
|
+
}
|
|
698
798
|
const parts = declaresShape(own)
|
|
699
|
-
? [objectKeys(own, defs, seen, depth)]
|
|
799
|
+
? [objectKeys(own, defs, seen, depth, work)]
|
|
700
800
|
: [];
|
|
701
801
|
for (const member of s.allOf) {
|
|
702
|
-
|
|
802
|
+
const keys = objectKeys(member, defs, seen, depth + 1, work);
|
|
803
|
+
if (!keys)
|
|
804
|
+
return undefined;
|
|
805
|
+
parts.push(keys);
|
|
703
806
|
}
|
|
704
807
|
// An allOf whose members are not all object shapes renders as an
|
|
705
808
|
// intersection with a non-object half; no single key list describes it.
|
|
@@ -707,15 +810,17 @@ function objectKeys(schema, defs, seen, depth) {
|
|
|
707
810
|
? mergedKeys(parts)
|
|
708
811
|
: undefined;
|
|
709
812
|
}
|
|
710
|
-
|
|
711
|
-
|
|
813
|
+
const reference = s.$ref ?? s.$dynamicRef;
|
|
814
|
+
if (typeof reference === "string") {
|
|
815
|
+
const rawName = refName(work.text(reference));
|
|
816
|
+
const name = s.$ref === undefined ? rawName.replace(/^#/, "") : rawName;
|
|
712
817
|
if (seen.has(name))
|
|
713
818
|
return undefined;
|
|
714
|
-
const target = defs
|
|
819
|
+
const target = resolveDefinition(defs, name);
|
|
715
820
|
if (target === undefined)
|
|
716
821
|
return undefined;
|
|
717
822
|
seen.add(name);
|
|
718
|
-
const resolved = objectKeys(target, defs, seen, depth);
|
|
823
|
+
const resolved = objectKeys(target, defs, seen, depth, work);
|
|
719
824
|
seen.delete(name);
|
|
720
825
|
return resolved;
|
|
721
826
|
}
|
|
@@ -725,19 +830,39 @@ function objectKeys(schema, defs, seen, depth) {
|
|
|
725
830
|
return undefined;
|
|
726
831
|
if (s.const !== undefined)
|
|
727
832
|
return undefined;
|
|
728
|
-
if (s.type === "array" || s.items)
|
|
833
|
+
if (s.type === "array" || s.items || Array.isArray(s.prefixItems))
|
|
729
834
|
return undefined;
|
|
730
835
|
if (s.type === "object" || s.properties) {
|
|
731
836
|
const props = s.properties;
|
|
732
837
|
if (props === null || Array.isArray(props) || typeof props !== "object") {
|
|
733
838
|
return { properties: [], required: [] };
|
|
734
839
|
}
|
|
840
|
+
const properties = propertyNames(props, work);
|
|
841
|
+
const declared = new Set(properties);
|
|
735
842
|
return {
|
|
736
|
-
properties
|
|
737
|
-
required:
|
|
738
|
-
? s.required.filter((key) => typeof key === "string")
|
|
739
|
-
: [],
|
|
843
|
+
properties,
|
|
844
|
+
required: schemaRequired(s, work).filter((key) => declared.has(key)),
|
|
740
845
|
};
|
|
741
846
|
}
|
|
742
847
|
return undefined;
|
|
743
848
|
}
|
|
849
|
+
function propertyNames(props, work) {
|
|
850
|
+
const names = [];
|
|
851
|
+
for (const name in props) {
|
|
852
|
+
work.visit();
|
|
853
|
+
if (Object.hasOwn(props, name))
|
|
854
|
+
names.push(work.text(name));
|
|
855
|
+
}
|
|
856
|
+
return names;
|
|
857
|
+
}
|
|
858
|
+
function schemaRequired(schema, work) {
|
|
859
|
+
if (!Array.isArray(schema.required))
|
|
860
|
+
return [];
|
|
861
|
+
const names = [];
|
|
862
|
+
for (const key of schema.required) {
|
|
863
|
+
work.visit();
|
|
864
|
+
if (typeof key === "string")
|
|
865
|
+
names.push(work.text(key));
|
|
866
|
+
}
|
|
867
|
+
return names;
|
|
868
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ToolAccess } from "./registry.js";
|
|
2
|
+
import type { AuthenticatedIdentity } from "./types.js";
|
|
3
|
+
/** A declared pool after construction-time validation. */
|
|
4
|
+
export interface ResolvedPool {
|
|
5
|
+
access: ConnectorAccess;
|
|
6
|
+
grant(identity: Readonly<AuthenticatedIdentity>): boolean | Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
export declare const POOL_NAME_RE: RegExp;
|
|
9
|
+
/**
|
|
10
|
+
* One derived view: which connectors, and for connectors granted by address
|
|
11
|
+
* only, which tools. A connector absent from `toolAccess` is visible whole.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConnectorAccess {
|
|
14
|
+
connectorIds: "all" | readonly string[];
|
|
15
|
+
toolAccess?: ToolAccess;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Normalize a grant list. A bare connector id grants every tool on that
|
|
19
|
+
* connector; a `connector.tool` address grants one tool. Grants are additive,
|
|
20
|
+
* so a bare id beside addresses for the same connector means the whole
|
|
21
|
+
* connector. Anything else — an unknown shape, an empty tool name, a
|
|
22
|
+
* non-string — throws, and the caller decides whether that is a construction
|
|
23
|
+
* failure or a 403: a grant that cannot be parsed must never fail open.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseConnectorAccess(value: unknown): ConnectorAccess;
|
|
26
|
+
/**
|
|
27
|
+
* The view a pool endpoint serves: the pool's grants, never wider than the
|
|
28
|
+
* identity's own. A connector or tool outside either side is gone; a
|
|
29
|
+
* connector whose tool intersection is empty is gone too, so the pool can
|
|
30
|
+
* only narrow what the identity resolver already allowed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function intersectAccess(ceiling: ConnectorAccess, pool: ConnectorAccess): ConnectorAccess;
|