@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/dist/auth/bearer.js +2 -0
  3. package/dist/auth/clerk.d.ts +0 -5
  4. package/dist/auth/clerk.js +21 -8
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/call-admission.d.ts +4 -0
  8. package/dist/call-admission.js +26 -0
  9. package/dist/catalog-drift.js +9 -4
  10. package/dist/catalog-service.d.ts +2 -0
  11. package/dist/catalog-service.js +25 -8
  12. package/dist/catalog.d.ts +2 -0
  13. package/dist/catalog.js +246 -121
  14. package/dist/connector-access.d.ts +32 -0
  15. package/dist/connector-access.js +79 -0
  16. package/dist/connectors/api.js +11 -1
  17. package/dist/connectors/guarded-fetch.d.ts +1 -1
  18. package/dist/connectors/guarded-fetch.js +27 -20
  19. package/dist/connectors/remote-mcp.js +84 -53
  20. package/dist/errors.d.ts +17 -0
  21. package/dist/errors.js +58 -0
  22. package/dist/execute.js +85 -23
  23. package/dist/executor-result.js +3 -1
  24. package/dist/executors/quickjs-child.js +5 -1
  25. package/dist/executors/quickjs-protocol.d.ts +4 -0
  26. package/dist/executors/quickjs-runtime.d.ts +1 -1
  27. package/dist/executors/quickjs-runtime.js +38 -21
  28. package/dist/executors/quickjs.js +68 -27
  29. package/dist/index.d.ts +37 -1
  30. package/dist/index.js +89 -3
  31. package/dist/invocation.js +134 -93
  32. package/dist/mcp-result.js +3 -2
  33. package/dist/meta-tools.js +118 -39
  34. package/dist/registry.d.ts +29 -1
  35. package/dist/registry.js +122 -15
  36. package/dist/routes/credentials.js +1 -0
  37. package/dist/routes/mcp.d.ts +4 -1
  38. package/dist/routes/mcp.js +112 -12
  39. package/dist/routes/oauth-management.js +1 -0
  40. package/dist/routes/oauth.js +4 -0
  41. package/dist/routes/shared.d.ts +7 -1
  42. package/dist/routes/shared.js +12 -13
  43. package/dist/routes/ui.js +2 -1
  44. package/dist/server.js +15 -3
  45. package/dist/skills.js +6 -5
  46. package/dist/storage/file.d.ts +6 -2
  47. package/dist/storage/file.js +312 -34
  48. package/dist/storage/memory.js +12 -1
  49. package/dist/validate.js +3 -3
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/documentation/architecture.md +30 -9
  53. package/documentation/auth.md +110 -6
  54. package/documentation/call-admission.md +24 -8
  55. package/documentation/code-mode.md +34 -22
  56. package/documentation/connectors.md +47 -5
  57. package/documentation/meta-tools.md +74 -6
  58. package/documentation/operations.md +20 -19
  59. package/documentation/provider-conventions.md +7 -0
  60. package/documentation/request-admission.md +38 -4
  61. package/documentation/storage-and-credentials.md +54 -1
  62. package/documentation/upgrading.md +21 -5
  63. package/ethos.md +1 -1
  64. package/package.json +1 -1
  65. 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;
@@ -1,4 +1,4 @@
1
- import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
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
- const offset = Math.max(0, Math.trunc(args.offset ?? 0));
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 requiredReasons = guideRequiredReasons(addressResolution.connector, tool, false);
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: renderSchema(input, format),
804
+ inputSchema: compactInput?.text ?? input,
805
+ ...(compactInput?.truncated ? { inputSchemaTruncated: true } : {}),
806
+ ...(compactOutput?.truncated ? { outputSchemaTruncated: true } : {}),
790
807
  ...(output.schema
791
808
  ? {
792
- outputSchema: renderSchema(output.schema, format),
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 renderedValues = values.map((value) => JSON.stringify(value));
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 < renderedValues.length - 1; index += 1) {
359
- prefix.push(renderedValues[index]);
360
- const omitted = renderedValues.length - prefix.length;
361
- const candidate = `(${prefix.join(" | ")} | ${marker(omitted)})`;
362
- if (schemaEncoder.encode(candidate).length > byteLimit)
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
- rendered = candidate;
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 JSON.stringify(schema);
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 { allOf: _members, ...own } = s;
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
- if (typeof s.$ref === "string") {
451
- const name = refName(s.$ref);
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[name];
455
- if (target === undefined)
456
- return name;
457
- seen.add(name);
458
- const rendered = renderSchema(target, defs, seen, depth, options);
459
- seen.delete(name);
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 = JSON.stringify(s.const);
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((Array.isArray(s.required) ? s.required : []));
492
- const declaredKeys = Object.keys(props);
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.join(" | ");
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 JSON.stringify(schema);
640
+ return options.work.json(schema);
524
641
  }
525
642
  const compactSchemas = new WeakMap();
526
- function defsOf(schema) {
527
- return {
528
- ...schema.$defs,
529
- ...schema.definitions,
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 defs = defsOf(schema);
538
- let rendered;
539
- try {
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
- const keys = schemaObjectKeys(schema);
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 defs = defsOf(schema);
598
- let rendered;
599
- let enumTruncated = false;
600
- let constraintTruncated = false;
601
- const base = {
602
- propertyDescriptions: false,
603
- requiredFirst: true,
604
- enumByteLimit: MAX_COMPACT_DISCOVERY_ENUM_BYTES,
605
- onEnumTruncated: () => {
606
- enumTruncated = true;
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
- rendered = renderSchema(schema, defs, new Set(), 0, {
611
- ...base,
612
- // Three near-cap enums spend about three quarters of the complete shape
613
- // budget, leaving the final quarter for surrounding syntax before the
614
- // unchanged global fallback applies. Whole values keep this UTF-8 safe.
615
- renderConstraints: true,
616
- constraintByteLimit: MAX_COMPACT_DISCOVERY_CONSTRAINT_BYTES,
617
- onConstraintTruncated: () => {
618
- constraintTruncated = true;
619
- },
620
- });
621
- }
622
- catch {
623
- rendered = JSON.stringify(schema);
624
- }
625
- if (schemaEncoder.encode(rendered).length >
626
- MAX_COMPACT_DISCOVERY_SCHEMA_BYTES) {
627
- try {
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, defs, new Set(), 0);
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 { allOf: _members, ...own } = s;
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
- parts.push(objectKeys(member, defs, seen, depth + 1));
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
- if (typeof s.$ref === "string") {
711
- const name = refName(s.$ref);
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[name];
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: Object.keys(props),
737
- required: Array.isArray(s.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;