@cerebro-labs/agentnava-sdk 0.44.1 → 0.46.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/README.md +11 -3
- package/agent.d.ts +14 -1
- package/agent.js +15 -0
- package/agent.js.map +1 -1
- package/import-tools.d.ts +6 -0
- package/import-tools.js +330 -82
- package/import-tools.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/types.d.ts +55 -2
- package/types.js.map +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
package/import-tools.js
CHANGED
|
@@ -426,113 +426,325 @@ export function fromPostman(collection, options = {}) {
|
|
|
426
426
|
return draft.finish(options);
|
|
427
427
|
}
|
|
428
428
|
const OPENAPI_METHODS = ['get', 'post', 'put', 'delete', 'patch'];
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
validateReference?.(value);
|
|
433
|
-
if (!value.$ref.startsWith('#/') || seen.has(value.$ref))
|
|
429
|
+
/** The object a local `#/...` reference names, one hop. */
|
|
430
|
+
function refTarget(ref, doc) {
|
|
431
|
+
if (!ref.startsWith('#/'))
|
|
434
432
|
throw Error('Only acyclic local OpenAPI references are supported');
|
|
435
|
-
seen.add(value.$ref);
|
|
436
433
|
let target = doc;
|
|
437
|
-
for (const key of
|
|
434
|
+
for (const key of ref.slice(2).split('/').map(x => x.replace(/~1/g, '/').replace(/~0/g, '~'))) {
|
|
438
435
|
if (!target || typeof target !== 'object' || !Object.hasOwn(target, key))
|
|
439
|
-
throw Error(`Unresolved OpenAPI reference ${
|
|
436
|
+
throw Error(`Unresolved OpenAPI reference ${ref}`);
|
|
440
437
|
target = target[key];
|
|
441
438
|
}
|
|
442
439
|
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
443
440
|
throw Error('OpenAPI reference must resolve to an object');
|
|
444
|
-
return
|
|
441
|
+
return target;
|
|
442
|
+
}
|
|
443
|
+
function dereference(value, doc, seen = new Set()) {
|
|
444
|
+
if (!value.$ref)
|
|
445
|
+
return value;
|
|
446
|
+
if (seen.has(value.$ref))
|
|
447
|
+
throw Error('Only acyclic local OpenAPI references are supported');
|
|
448
|
+
seen.add(value.$ref);
|
|
449
|
+
return dereference(refTarget(value.$ref, doc), doc, seen);
|
|
450
|
+
}
|
|
451
|
+
/** Keywords that only narrow which values are valid: a bound, a length, a
|
|
452
|
+
* pattern, a format. A tool parameter has no field for them, so they go into
|
|
453
|
+
* its description for the model, and the API enforces them, as it does for
|
|
454
|
+
* every other caller.
|
|
455
|
+
*
|
|
456
|
+
* THEY USED TO SKIP THE WHOLE OPERATION, even on an optional parameter, on the
|
|
457
|
+
* theory that a tool which cannot enforce a rule must not exist. Real documents
|
|
458
|
+
* put these on almost every field: a generated document with 188 operations
|
|
459
|
+
* drafted 77 tools. The rule was never the tool's to enforce; the API already
|
|
460
|
+
* does, and its 400 tells the model what to fix. */
|
|
461
|
+
const RULE_KEYWORDS = [
|
|
462
|
+
'minimum', 'exclusiveMinimum', 'maximum', 'exclusiveMaximum', 'multipleOf', 'minLength', 'maxLength',
|
|
463
|
+
'pattern', 'format', 'minItems', 'maxItems', 'uniqueItems', 'minProperties', 'maxProperties',
|
|
464
|
+
];
|
|
465
|
+
const PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'boolean']);
|
|
466
|
+
const PARAM_TYPES = new Set([...PRIMITIVE_TYPES, 'object', 'array']);
|
|
467
|
+
const TYPE_PHRASE = {
|
|
468
|
+
string: 'a string', number: 'a number', integer: 'an integer', boolean: 'true or false', object: 'an object', array: 'a list',
|
|
469
|
+
};
|
|
470
|
+
const refName = (ref) => ref.slice(ref.lastIndexOf('/') + 1);
|
|
471
|
+
/** Sentences joined into one description. A lone part gains no full stop. */
|
|
472
|
+
function sentences(...parts) {
|
|
473
|
+
const kept = parts.map(p => p?.trim()).filter((p) => !!p);
|
|
474
|
+
if (kept.length <= 1)
|
|
475
|
+
return kept[0];
|
|
476
|
+
return kept.map(p => /[.!?]$/.test(p) ? p : `${p}.`).join(' ');
|
|
477
|
+
}
|
|
478
|
+
/** The type a resolved schema declares, or the one its other keywords imply. */
|
|
479
|
+
function typeOf(s) {
|
|
480
|
+
if (typeof s.type === 'string')
|
|
481
|
+
return s.type;
|
|
482
|
+
if (s.properties !== undefined || s.additionalProperties !== undefined)
|
|
483
|
+
return 'object';
|
|
484
|
+
if (s.items !== undefined)
|
|
485
|
+
return 'array';
|
|
486
|
+
const values = s.enum?.filter(v => v !== null) ?? [];
|
|
487
|
+
if (values.length && values.every(v => typeof v === 'string'))
|
|
488
|
+
return 'string';
|
|
489
|
+
if (values.length && values.every(v => typeof v === 'boolean'))
|
|
490
|
+
return 'boolean';
|
|
491
|
+
if (values.length && values.every(v => typeof v === 'number'))
|
|
492
|
+
return values.every(Number.isInteger) ? 'integer' : 'number';
|
|
493
|
+
return undefined;
|
|
445
494
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
495
|
+
/** The rules a schema carries, in words: "at least 1", "3 to 64 characters". */
|
|
496
|
+
function ruleText(s, type) {
|
|
497
|
+
const n = (key) => typeof s[key] === 'number' && Number.isFinite(s[key]) ? s[key] : undefined;
|
|
498
|
+
const span = (min, max, unit) => {
|
|
499
|
+
const of = (x) => `${x} ${unit}${x === 1 ? '' : 's'}`;
|
|
500
|
+
if (min !== undefined && max !== undefined)
|
|
501
|
+
return min === max ? `exactly ${of(min)}` : `${min} to ${of(max)}`;
|
|
502
|
+
if (min !== undefined)
|
|
503
|
+
return `at least ${of(min)}`;
|
|
504
|
+
return max !== undefined ? `at most ${of(max)}` : undefined;
|
|
505
|
+
};
|
|
506
|
+
const extra = s.additionalProperties;
|
|
507
|
+
const value = extra && typeof extra === 'object'
|
|
508
|
+
? (typeof extra.type === 'string' ? TYPE_PHRASE[extra.type] : typeof extra.$ref === 'string' ? refName(extra.$ref) : undefined)
|
|
509
|
+
: undefined;
|
|
510
|
+
return [
|
|
511
|
+
// 3.0 writes `exclusiveMinimum: true` beside `minimum`; 3.1 makes it the bound.
|
|
512
|
+
n('minimum') !== undefined ? `${s.exclusiveMinimum === true ? 'more than' : 'at least'} ${n('minimum')}` : undefined,
|
|
513
|
+
n('exclusiveMinimum') !== undefined ? `more than ${n('exclusiveMinimum')}` : undefined,
|
|
514
|
+
n('maximum') !== undefined ? `${s.exclusiveMaximum === true ? 'less than' : 'at most'} ${n('maximum')}` : undefined,
|
|
515
|
+
n('exclusiveMaximum') !== undefined ? `less than ${n('exclusiveMaximum')}` : undefined,
|
|
516
|
+
n('multipleOf') !== undefined ? `a multiple of ${n('multipleOf')}` : undefined,
|
|
517
|
+
span(n('minLength'), n('maxLength'), 'character'),
|
|
518
|
+
typeof s.pattern === 'string' ? `pattern \`${s.pattern}\`` : undefined,
|
|
519
|
+
// int32, int64 and double say how a number is stored, not what to send.
|
|
520
|
+
typeof s.format === 'string' && type === 'string' ? `format ${s.format}` : undefined,
|
|
521
|
+
span(n('minItems'), n('maxItems'), 'item'),
|
|
522
|
+
s.uniqueItems === true ? 'no duplicate items' : undefined,
|
|
523
|
+
span(n('minProperties'), n('maxProperties'), 'field'),
|
|
524
|
+
extra && typeof extra === 'object' ? `any keys${value ? `, each value ${value}` : ''}` : undefined,
|
|
525
|
+
].filter((rule) => rule !== undefined);
|
|
454
526
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
527
|
+
/** One flat schema, every reference, composition and alternative folded in.
|
|
528
|
+
* Rules stay on it as keywords, for `ruleText` to put into words; nested
|
|
529
|
+
* properties and items are left for their own turn.
|
|
530
|
+
*
|
|
531
|
+
* `nesting` counts references and compositions, separately from how deep the
|
|
532
|
+
* parameter itself goes: the API allows 12 levels of fields, and a document
|
|
533
|
+
* that reaches each level through a $ref and an allOf should not spend three. */
|
|
534
|
+
function resolveOpenApiSchema(raw, ctx, where, nesting = 0) {
|
|
535
|
+
if (nesting > 32)
|
|
536
|
+
throw Error('OpenAPI schema is recursive or nests references more than 32 deep');
|
|
537
|
+
if (typeof raw.$ref === 'string') {
|
|
538
|
+
const { $ref, ...siblings } = raw;
|
|
539
|
+
const target = refTarget($ref, ctx.doc);
|
|
540
|
+
// Keywords beside a $ref apply together with what it names (OpenAPI 3.1),
|
|
541
|
+
// which is also what a 3.0 author writing a description or a rule there means.
|
|
542
|
+
const next = Object.keys(siblings).length ? { ...siblings, allOf: [target, ...(siblings.allOf ?? [])] } : target;
|
|
543
|
+
return resolveOpenApiSchema(next, ctx, where, nesting + 1);
|
|
544
|
+
}
|
|
545
|
+
const { allOf, oneOf, anyOf, ...own } = raw;
|
|
546
|
+
if (allOf && !allOf.length)
|
|
547
|
+
throw Error('An allOf schema needs at least one member');
|
|
548
|
+
const parts = (allOf ?? []).map(part => resolveOpenApiSchema(part, ctx, where, nesting + 1));
|
|
549
|
+
const said = [];
|
|
550
|
+
const join = (alternatives) => {
|
|
551
|
+
const joined = joinAlternatives(alternatives, ctx, where, nesting + 1);
|
|
552
|
+
parts.push(joined.schema);
|
|
553
|
+
if (joined.text)
|
|
554
|
+
said.push(joined.text);
|
|
555
|
+
};
|
|
556
|
+
if (Array.isArray(own.type)) {
|
|
557
|
+
// `type: [a, b]` is anyOf by another name, and "null" in it is nullable.
|
|
558
|
+
const types = own.type.filter(t => t !== 'null');
|
|
559
|
+
if (own.type.includes('null'))
|
|
560
|
+
own.nullable = true;
|
|
561
|
+
if (types.length === 1)
|
|
562
|
+
own.type = types[0];
|
|
563
|
+
else if (types.length) {
|
|
564
|
+
delete own.type;
|
|
565
|
+
join(types.map(type => ({ type })));
|
|
459
566
|
}
|
|
567
|
+
else
|
|
568
|
+
own.type = 'null';
|
|
460
569
|
}
|
|
570
|
+
if (oneOf !== undefined)
|
|
571
|
+
join(oneOf);
|
|
572
|
+
if (anyOf !== undefined)
|
|
573
|
+
join(anyOf);
|
|
574
|
+
const merged = intersect(parts, own);
|
|
575
|
+
const description = sentences(merged.description, ...said);
|
|
576
|
+
return description === undefined ? merged : { ...merged, description };
|
|
461
577
|
}
|
|
462
|
-
/**
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
578
|
+
/** allOf: every part applies at once. Properties and required fields add up,
|
|
579
|
+
* types and enums must agree, and rules ride along to be described. A genuine
|
|
580
|
+
* contradiction, a value that has to be both a string and a number, is an error
|
|
581
|
+
* rather than a guess. Parts are flat; their properties and items are not yet. */
|
|
582
|
+
function intersect(parts, own) {
|
|
583
|
+
const all = [...parts, own];
|
|
584
|
+
if (all.some(part => part.prefixItems !== undefined))
|
|
585
|
+
throw Error('Tuple arrays (prefixItems) are not supported');
|
|
586
|
+
// Annotations and rules: the outer schema's own word, else the first part's.
|
|
587
|
+
const first = (key) => [own, ...parts].map(part => part[key]).find(value => value !== undefined);
|
|
588
|
+
const types = [...new Set(all.map(part => part.type).filter((t) => typeof t === 'string'))];
|
|
589
|
+
const numeric = types.length === 2 && types.includes('integer') && types.includes('number');
|
|
590
|
+
if (types.length > 1 && !numeric)
|
|
591
|
+
throw Error(`allOf members disagree on the type (${types.join(' and ')})`);
|
|
592
|
+
const enums = all.map(part => part.const !== undefined ? [part.const] : part.enum).filter(e => e !== undefined);
|
|
593
|
+
const values = enums.length ? enums.reduce((kept, e) => kept.filter(v => e.includes(v))) : undefined;
|
|
594
|
+
if (values && !values.length)
|
|
595
|
+
throw Error('allOf members leave no allowed value');
|
|
475
596
|
const properties = {};
|
|
476
|
-
const
|
|
477
|
-
for (const part of parts) {
|
|
478
|
-
checkSchemaKeywords(part);
|
|
479
|
-
if ((part.type !== undefined && part.type !== 'object') || part.enum || part.nullable || part.items) {
|
|
480
|
-
throw Error('Only non-nullable object allOf composition is supported');
|
|
481
|
-
}
|
|
482
|
-
if (part.additionalProperties !== undefined && typeof part.additionalProperties !== 'boolean') {
|
|
483
|
-
throw Error('Schema-valued additionalProperties is not supported');
|
|
484
|
-
}
|
|
597
|
+
for (const part of all) {
|
|
485
598
|
for (const [key, property] of Object.entries(part.properties ?? {})) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
Object.defineProperty(properties, key, { value
|
|
599
|
+
const before = Object.hasOwn(properties, key) ? properties[key] : undefined;
|
|
600
|
+
// The same field from two parts must satisfy both, so its rules add up too.
|
|
601
|
+
const value = before === undefined || JSON.stringify(before) === JSON.stringify(property) ? property : { allOf: [before, property] };
|
|
602
|
+
Object.defineProperty(properties, key, { value, enumerable: true, configurable: true, writable: true });
|
|
490
603
|
}
|
|
491
|
-
for (const key of part.required ?? [])
|
|
492
|
-
required.add(key);
|
|
493
604
|
}
|
|
494
|
-
const
|
|
495
|
-
|
|
605
|
+
const extras = all.map(part => part.additionalProperties).filter(extra => extra !== undefined);
|
|
606
|
+
// Keys that match patternProperties are allowed even on a closed object.
|
|
607
|
+
const closed = extras.includes(false) && !all.some(part => part.patternProperties !== undefined);
|
|
608
|
+
if (closed && all.some(part => part.additionalProperties === false && Object.keys(properties).some(key => !Object.hasOwn(part.properties ?? {}, key)))) {
|
|
496
609
|
throw Error('Extending a closed object with allOf is not supported');
|
|
497
610
|
}
|
|
611
|
+
const items = all.map(part => part.items).filter((item) => item !== undefined);
|
|
612
|
+
const required = [...new Set(all.flatMap(part => part.required ?? []))];
|
|
613
|
+
const annotations = Object.fromEntries(['title', 'description', ...RULE_KEYWORDS]
|
|
614
|
+
.map(key => [key, first(key)]).filter(([, value]) => value !== undefined));
|
|
498
615
|
return {
|
|
499
|
-
...
|
|
500
|
-
type: '
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
...(
|
|
616
|
+
...annotations,
|
|
617
|
+
...(types.length && { type: numeric ? 'integer' : types[0] }),
|
|
618
|
+
...(all.some(part => part.nullable === true) && { nullable: true }),
|
|
619
|
+
...(values && { enum: values }),
|
|
620
|
+
...(all.some(part => part.properties !== undefined) && { properties }),
|
|
621
|
+
...(required.length && { required }),
|
|
622
|
+
...(items.length && { items: items.length === 1 ? items[0] : { allOf: items } }),
|
|
623
|
+
...(extras.length && { additionalProperties: closed ? false : extras.find(extra => typeof extra === 'object') ?? true }),
|
|
504
624
|
// A read-only member still makes the composed property response-only.
|
|
505
|
-
...(
|
|
625
|
+
...(all.some(part => part.readOnly === true) && { readOnly: true }),
|
|
506
626
|
};
|
|
507
627
|
}
|
|
508
|
-
/**
|
|
509
|
-
*
|
|
510
|
-
|
|
628
|
+
/** oneOf / anyOf: one shape that admits every alternative, as closely as a tool
|
|
629
|
+
* parameter can say it, plus the alternatives in words for the model. The API
|
|
630
|
+
* then refuses a value that fits none of them, as it would from any caller. */
|
|
631
|
+
function joinAlternatives(raw, ctx, where, nesting) {
|
|
632
|
+
if (!raw.length)
|
|
633
|
+
throw Error('oneOf and anyOf need at least one alternative');
|
|
634
|
+
const resolved = raw.map(alternative => resolveOpenApiSchema(alternative, ctx, where, nesting));
|
|
635
|
+
const onlyNull = (s) => s.type === 'null' || (!!s.enum?.length && s.enum.every(v => v === null));
|
|
636
|
+
const options = resolved.flatMap((schema, i) => onlyNull(schema) ? [] : [{ schema, raw: raw[i] }]);
|
|
637
|
+
if (!options.length)
|
|
638
|
+
throw Error('An alternative list that allows only null is not supported');
|
|
639
|
+
const nullable = options.length < resolved.length || options.some(option => option.schema.nullable === true);
|
|
640
|
+
const withNull = (schema) => nullable ? { ...schema, nullable: true } : schema;
|
|
641
|
+
// "X or null" is how most generators write an optional field.
|
|
642
|
+
if (options.length === 1)
|
|
643
|
+
return { schema: withNull(options[0].schema) };
|
|
644
|
+
const labels = options.map(option => alternativeLabel(option.raw, option.schema));
|
|
645
|
+
const types = [...new Set(options.map(option => typeOf(option.schema)).filter((t) => t !== undefined))];
|
|
646
|
+
const type = types.length <= 1 ? types[0]
|
|
647
|
+
: types.every(t => t === 'number' || t === 'integer') ? 'number'
|
|
648
|
+
// A string takes what any of these primitives would say, and on a query or
|
|
649
|
+
// path it is exactly what goes on the wire.
|
|
650
|
+
: types.includes('string') && types.every(t => PRIMITIVE_TYPES.has(t)) ? 'string'
|
|
651
|
+
: undefined;
|
|
652
|
+
if (types.length > 1 && type === undefined) {
|
|
653
|
+
// No one parameter takes both an object and a list. Every alternative is
|
|
654
|
+
// valid by the document's own account, so the first one is kept.
|
|
655
|
+
ctx.notes.add(`"${where}" accepts ${[...new Set(labels)].join(' or ')}. A tool parameter takes one shape, so it ` +
|
|
656
|
+
`sends ${labels[0]}; edit the tool if the agent needs another.`);
|
|
657
|
+
return { schema: withNull(options[0].schema) };
|
|
658
|
+
}
|
|
659
|
+
const all = options.map(option => option.schema);
|
|
660
|
+
const shared = new Map();
|
|
661
|
+
for (const schema of all) {
|
|
662
|
+
for (const [key, property] of Object.entries(schema.properties ?? {})) {
|
|
663
|
+
const seen = shared.get(key) ?? [];
|
|
664
|
+
if (!seen.some(s => JSON.stringify(s) === JSON.stringify(property)))
|
|
665
|
+
seen.push(property);
|
|
666
|
+
shared.set(key, seen);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const properties = {};
|
|
670
|
+
for (const [key, schemas] of shared) {
|
|
671
|
+
// A field several alternatives name may take any of their shapes.
|
|
672
|
+
Object.defineProperty(properties, key, { value: schemas.length === 1 ? schemas[0] : { anyOf: schemas }, enumerable: true, configurable: true, writable: true });
|
|
673
|
+
}
|
|
674
|
+
const required = all.map(schema => schema.required ?? []).reduce((kept, r) => kept.filter(key => r.includes(key)));
|
|
675
|
+
// Choices only when every alternative lists them, and only in one type.
|
|
676
|
+
const values = all.every(schema => schema.enum) && (types.length <= 1 || type === 'number')
|
|
677
|
+
? [...new Set(all.flatMap(schema => schema.enum))] : undefined;
|
|
678
|
+
const items = all.map(schema => schema.items).filter((item) => item !== undefined);
|
|
679
|
+
const named = options.some(option => option.raw.$ref !== undefined || option.schema.title !== undefined);
|
|
680
|
+
const said = new Set(labels).size > 1 && !(values && !named);
|
|
681
|
+
return {
|
|
682
|
+
schema: withNull({
|
|
683
|
+
...(type && { type }),
|
|
684
|
+
...(shared.size && { properties }),
|
|
685
|
+
...(required.length && { required }),
|
|
686
|
+
...(values && { enum: values }),
|
|
687
|
+
...(items.length && { items: items.length === 1 ? items[0] : { anyOf: items } }),
|
|
688
|
+
...(all.every(schema => schema.additionalProperties === false) && { additionalProperties: false }),
|
|
689
|
+
}),
|
|
690
|
+
...(said && { text: `One of: ${labels.join('; ')}.` }),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/** One alternative in a few words: its name, then its fields or its rules. */
|
|
694
|
+
function alternativeLabel(raw, s) {
|
|
695
|
+
const name = typeof raw.$ref === 'string' ? refName(raw.$ref) : typeof s.title === 'string' ? s.title : undefined;
|
|
696
|
+
const type = typeOf(s);
|
|
697
|
+
const fields = s.required?.length ? s.required : Object.keys(s.properties ?? {});
|
|
698
|
+
const shown = `with ${[...fields.slice(0, 8), ...(fields.length > 8 ? ['…'] : [])].join(', ')}`;
|
|
699
|
+
// A fragment that only says which fields are required reads as "with email".
|
|
700
|
+
const detail = type === 'object' || (type === undefined && fields.length)
|
|
701
|
+
? (!fields.length ? TYPE_PHRASE.object : name || type === undefined ? shown : `${TYPE_PHRASE.object} ${shown}`)
|
|
702
|
+
: [
|
|
703
|
+
s.enum?.length === 1 ? JSON.stringify(s.enum[0]) : s.enum ? `one of ${s.enum.map(v => JSON.stringify(v)).join(', ')}` : TYPE_PHRASE[type ?? ''],
|
|
704
|
+
...ruleText(s, type),
|
|
705
|
+
].filter(Boolean).join(', ');
|
|
706
|
+
return name && detail ? `${name} (${detail})` : name ?? (detail || 'any value');
|
|
707
|
+
}
|
|
708
|
+
/** Swagger 2 writes a non-body parameter's schema on the parameter itself. */
|
|
709
|
+
function swaggerParamSchema(p) {
|
|
710
|
+
const schema = { type: p.type ?? 'string' };
|
|
711
|
+
for (const key of ['enum', 'items', ...RULE_KEYWORDS])
|
|
712
|
+
if (p[key] !== undefined)
|
|
713
|
+
schema[key] = p[key];
|
|
714
|
+
return schema;
|
|
715
|
+
}
|
|
716
|
+
/** A tool parameter from a schema. What a parameter cannot carry, a rule or a
|
|
717
|
+
* choice between shapes, is put into its description; a shape no parameter can
|
|
718
|
+
* take at all (a tuple, a recursive tree) still skips the operation, with a
|
|
719
|
+
* warning, rather than drafting a call that cannot be made. */
|
|
720
|
+
function openApiParam(raw, ctx, depth = 0, where = 'body') {
|
|
511
721
|
if (depth > 12)
|
|
512
722
|
throw Error('OpenAPI schema is recursive or exceeds 12 levels');
|
|
513
|
-
const s = resolveOpenApiSchema(raw,
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// discard validation rules from the source document.
|
|
517
|
-
checkSchemaKeywords(s);
|
|
518
|
-
const types = Array.isArray(s.type) ? s.type : [s.type ?? (s.properties ? 'object' : undefined)];
|
|
519
|
-
const concrete = types.filter(t => t !== 'null');
|
|
520
|
-
if (concrete.length !== 1 || !['string', 'number', 'integer', 'boolean', 'object', 'array'].includes(concrete[0] ?? ''))
|
|
723
|
+
const s = resolveOpenApiSchema(raw, ctx, where);
|
|
724
|
+
const t = typeOf(s);
|
|
725
|
+
if (t === undefined || !PARAM_TYPES.has(t))
|
|
521
726
|
throw Error('JSON schema must declare one supported type (optionally nullable)');
|
|
522
|
-
const
|
|
523
|
-
|
|
727
|
+
const type = (t === 'integer' ? 'number' : t);
|
|
728
|
+
// Choices the API would refuse, a value of another type, are described instead.
|
|
729
|
+
const fits = (v) => v === null || (t === 'integer' ? Number.isInteger(v) : typeof v === type);
|
|
730
|
+
const choices = s.enum?.length && PRIMITIVE_TYPES.has(t) && s.enum.every(fits) ? s.enum : undefined;
|
|
731
|
+
const rules = ruleText(s, t);
|
|
732
|
+
if (s.enum?.length && !choices)
|
|
733
|
+
rules.push(`one of ${s.enum.map(v => JSON.stringify(v)).join(', ')}`);
|
|
734
|
+
const description = sentences(s.description, rules.length ? `Rules: ${rules.join('; ')}.` : undefined);
|
|
735
|
+
const nullable = s.nullable === true || (choices?.includes(null) ?? false);
|
|
736
|
+
const param = { type, ...(t === 'integer' && { integer: true }), ...(description && { description }), ...(choices && { choices }), ...(nullable && { nullable: true }) };
|
|
524
737
|
if (t === 'object') {
|
|
525
738
|
if (Object.hasOwn(s.properties ?? {}, '__proto__'))
|
|
526
739
|
throw Error('Object property __proto__ is not supported');
|
|
527
|
-
|
|
528
|
-
throw Error('Schema-valued additionalProperties is not supported');
|
|
740
|
+
// A map (`additionalProperties` holding a schema) takes any key; its rule is described.
|
|
529
741
|
if (s.additionalProperties !== undefined)
|
|
530
|
-
param.additionalProperties = s.additionalProperties;
|
|
742
|
+
param.additionalProperties = s.additionalProperties !== false;
|
|
531
743
|
param.properties = Object.fromEntries(Object.entries(s.properties ?? {})
|
|
532
744
|
.filter(([, value]) => value.readOnly !== true)
|
|
533
|
-
.map(([key, value]) => [key, resolveOpenApiSchema(value,
|
|
745
|
+
.map(([key, value]) => [key, resolveOpenApiSchema(value, ctx, `${where}.${key}`)])
|
|
534
746
|
.filter(([, value]) => value.readOnly !== true)
|
|
535
|
-
.map(([key, value]) => [key, { ...openApiParam(value,
|
|
747
|
+
.map(([key, value]) => [key, { ...openApiParam(value, ctx, depth + 1, `${where}.${key}`), ...(s.required?.includes(key) && { required: true }) }]));
|
|
536
748
|
for (const key of s.required ?? [])
|
|
537
749
|
if (!Object.hasOwn(s.properties ?? {}, key))
|
|
538
750
|
throw Error(`Required property ${key} has no schema`);
|
|
@@ -540,7 +752,7 @@ function openApiParam(raw, doc, depth = 0) {
|
|
|
540
752
|
if (t === 'array') {
|
|
541
753
|
if (!s.items)
|
|
542
754
|
throw Error('Array schema needs items');
|
|
543
|
-
param.items = openApiParam(s.items,
|
|
755
|
+
param.items = openApiParam(s.items, ctx, depth + 1, `${where}[]`);
|
|
544
756
|
}
|
|
545
757
|
return param;
|
|
546
758
|
}
|
|
@@ -614,6 +826,39 @@ function openApiSecretNames(schemes, options) {
|
|
|
614
826
|
}
|
|
615
827
|
return allocate;
|
|
616
828
|
}
|
|
829
|
+
/** The security for an operation the document says nothing about, when it
|
|
830
|
+
* defines a scheme and no `security` array anywhere names one.
|
|
831
|
+
*
|
|
832
|
+
* REAL DOCUMENTS DO THIS ALL THE TIME: the scheme is declared under components
|
|
833
|
+
* and the `security` array is never written. Read literally, every operation is
|
|
834
|
+
* then public, which is how drafted tools came out with no credential and failed
|
|
835
|
+
* every call with a 401 nobody had been warned about. One scheme is unambiguous,
|
|
836
|
+
* so it applies, and the developer is told. With several, choosing would be a
|
|
837
|
+
* guess, so none applies and the warning says so. An operation that writes
|
|
838
|
+
* `security: []` stays public: that was said on purpose. */
|
|
839
|
+
function unlistedSecurity(doc, schemes, draft) {
|
|
840
|
+
const names = Object.keys(schemes);
|
|
841
|
+
const operations = Object.values(doc.paths ?? {}).flatMap(item => OPENAPI_METHODS.flatMap(method => item?.[method] ?? []));
|
|
842
|
+
const named = [doc.security, ...operations.map(op => op.security)].some(list => Array.isArray(list) && list.some(requirement => !!requirement && Object.keys(requirement).length > 0));
|
|
843
|
+
// Only an operation the document says nothing about takes the default.
|
|
844
|
+
const silent = doc.security === undefined && operations.some(op => op.security === undefined);
|
|
845
|
+
if (!names.length || named || !silent)
|
|
846
|
+
return [];
|
|
847
|
+
const only = names.length === 1 ? names[0] : undefined;
|
|
848
|
+
if (only !== undefined && supportedSecurityScheme(schemes[only])) {
|
|
849
|
+
draft.warnings.push({ kind: 'security-choice', message: `The document defines one security scheme, "${only}", and no operation lists it under "security", ` +
|
|
850
|
+
`so every tool whose operation does not set "security" sends it. Remove it from any tool that calls ` +
|
|
851
|
+
`a public endpoint.` });
|
|
852
|
+
return [{ [only]: [] }];
|
|
853
|
+
}
|
|
854
|
+
draft.warnings.push({ kind: 'security-choice', message: only !== undefined
|
|
855
|
+
? `The document defines the security scheme "${only}" (${schemes[only]?.type ?? 'no type'}), which the import cannot set up, ` +
|
|
856
|
+
`and no operation lists it, so the tools send no credential. Add the header the API expects to each tool.`
|
|
857
|
+
: `The document defines ${names.length} security schemes (${names.map(n => `"${n}"`).join(', ')}) and no operation lists one ` +
|
|
858
|
+
`under "security", so the tools send no credential. List the one the API uses under "security" and import again, ` +
|
|
859
|
+
`or add the header to each tool.` });
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
617
862
|
/** Draft supported OpenAPI/Swagger requests; authentication follows each
|
|
618
863
|
* operation's security requirement, including AND, alternatives and security:[]. */
|
|
619
864
|
export function fromOpenApi(doc, options = {}) {
|
|
@@ -623,6 +868,7 @@ export function fromOpenApi(doc, options = {}) {
|
|
|
623
868
|
draft.warnings.push({ kind: 'undefined-variable', message: 'The document names no server. Set absolute HTTP(S) tool URLs before creating the agent.' });
|
|
624
869
|
const schemes = doc.components?.securitySchemes ?? doc.securityDefinitions ?? {};
|
|
625
870
|
const secretName = openApiSecretNames(schemes, options);
|
|
871
|
+
const unlisted = unlistedSecurity(doc, schemes, draft);
|
|
626
872
|
for (const [path, item] of Object.entries(doc.paths ?? {}))
|
|
627
873
|
for (const method of OPENAPI_METHODS) {
|
|
628
874
|
const op = item[method];
|
|
@@ -633,12 +879,13 @@ export function fromOpenApi(doc, options = {}) {
|
|
|
633
879
|
continue;
|
|
634
880
|
const name = draft.uniqueName(candidate);
|
|
635
881
|
try {
|
|
882
|
+
const ctx = { doc, notes: new Set() };
|
|
636
883
|
const merged = new Map();
|
|
637
884
|
for (const raw of [...(item.parameters ?? []), ...(op.parameters ?? [])]) {
|
|
638
885
|
const p = dereference(raw, doc);
|
|
639
886
|
merged.set(p.in + ':' + p.name, p);
|
|
640
887
|
}
|
|
641
|
-
const security = op.security ?? doc.security ??
|
|
888
|
+
const security = op.security ?? doc.security ?? unlisted;
|
|
642
889
|
const selected = selectSecurity(security, schemes, options, merged.values());
|
|
643
890
|
const keys = Object.keys(security[selected] ?? {});
|
|
644
891
|
const credentialDestinations = new Set(keys.map(key => securityDestination(schemes[key])));
|
|
@@ -661,10 +908,9 @@ export function fromOpenApi(doc, options = {}) {
|
|
|
661
908
|
}
|
|
662
909
|
if (!['path', 'query', 'header', 'body'].includes(p.in ?? ''))
|
|
663
910
|
throw Error(`Unsupported parameter location ${p.in}`);
|
|
664
|
-
const schema = p.schema ??
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
def.description = p.description;
|
|
911
|
+
const schema = p.schema ?? swaggerParamSchema(p);
|
|
912
|
+
// The parameter's own description leads, and the schema's rules follow it.
|
|
913
|
+
const def = openApiParam(p.description ? { ...schema, description: p.description } : schema, ctx, 0, p.name);
|
|
668
914
|
if (p.required || p.in === 'path')
|
|
669
915
|
def.required = true;
|
|
670
916
|
if (p.in !== 'body' && (['object', 'array'].includes(def.type) || (p.style && !['simple', 'form'].includes(p.style))))
|
|
@@ -688,7 +934,7 @@ export function fromOpenApi(doc, options = {}) {
|
|
|
688
934
|
const media = Object.entries(requestBody.content ?? {}).find(([type]) => /(?:\/json|\+json)$/.test(type));
|
|
689
935
|
if (!media?.[1].schema)
|
|
690
936
|
throw Error('Only a JSON request body with a declared schema is supported');
|
|
691
|
-
const def = openApiParam(media[1].schema,
|
|
937
|
+
const def = openApiParam(media[1].schema, ctx);
|
|
692
938
|
if (requestBody.required)
|
|
693
939
|
def.required = true;
|
|
694
940
|
const param = draft.param(params, 'body', def, name, 'requestBody');
|
|
@@ -711,6 +957,8 @@ export function fromOpenApi(doc, options = {}) {
|
|
|
711
957
|
if (query.length)
|
|
712
958
|
url += '?' + query.join('&');
|
|
713
959
|
draft.tools.push({ name, description: describe([op.description, op.summary], name, method.toUpperCase(), path, draft), method: method.toUpperCase(), url, ...(Object.keys(headers).length && { headers }), ...(Object.keys(params).length && { params }), ...(draft.secretsFor(name).length && { secrets: draft.secretsFor(name) }), ...(body !== undefined && method !== 'get' && { body }) });
|
|
960
|
+
for (const note of ctx.notes)
|
|
961
|
+
draft.warnings.push({ kind: 'unsupported-body', tool: name, message: note });
|
|
714
962
|
}
|
|
715
963
|
catch (error) {
|
|
716
964
|
draft.warnings.push({ kind: 'unsupported-operation', tool: name, message: `Skipped ${method.toUpperCase()} ${path}: ${error.message}. Configure this operation explicitly.` });
|