@m6d/cortex-cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { canCarrySecret } from "../egress-policy/index";
4
4
  import {
5
5
  agentSlugSchema,
6
6
  EVENT_HEADER,
7
- IDENTIFIER_PATTERN,
7
+ identifierSchema,
8
8
  localeSchema,
9
9
  SERVER_EVENTS,
10
10
  refuseBlockedApprovalTitle,
@@ -12,6 +12,8 @@ import {
12
12
  } from "../server/index";
13
13
  import { MESSAGE_TRIGGER_NAME } from "../triggers/index";
14
14
  import {
15
+ BASE_LOCALE,
16
+ byLocale,
15
17
  DEFAULT_MEDIA_FORMS,
16
18
  MEDIA_FORMS,
17
19
  TOOL_SURFACES,
@@ -60,8 +62,9 @@ type Bounds = { min: number; max: number; default: number };
60
62
 
61
63
  /**
62
64
  * The range of every number an author sets, in the console and in a
63
- * manifest alike, and the value a manifest that leaves one out gets. The
64
- * console builds its form schemas, with its own messages, from these.
65
+ * manifest alike, and the value a manifest that leaves one out gets, which
66
+ * a new row's column holds too. The console builds its form schemas, with
67
+ * its own messages, from these.
65
68
  */
66
69
  export const SETTING_BOUNDS = {
67
70
  tool: {
@@ -99,6 +102,16 @@ function setting({ min, max, default: fallback }: Bounds) {
99
102
  return z.number().int().min(min).max(max).default(fallback);
100
103
  }
101
104
 
105
+ /** One number per setting a group of `SETTING_BOUNDS` names. */
106
+ export type SettingValues<TBounds> = { [Name in keyof TBounds]: number };
107
+
108
+ /** Every setting of a group at its default, as a row that sets none stores them. */
109
+ export function settingDefaults<TBounds extends Record<string, Bounds>>(bounds: TBounds) {
110
+ return Object.fromEntries(
111
+ Object.entries(bounds).map(([name, bound]) => [name, bound.default]),
112
+ ) as SettingValues<TBounds>;
113
+ }
114
+
102
115
  /**
103
116
  * The fields of an entry users may change in Cortex after an apply, each
104
117
  * once, from the ones `fields` allows; none when left out. Every other field
@@ -111,16 +124,14 @@ function setting({ min, max, default: fallback }: Bounds) {
111
124
  function overridable<const TField extends string>(fields: readonly [TField, ...TField[]]) {
112
125
  return z
113
126
  .array(z.enum(fields))
114
- .refine(
115
- uniqueBy((field: TField) => field),
116
- "a field repeats",
117
- )
127
+ .refine((listed) => new Set(listed).size === listed.length, "a field repeats")
118
128
  .default([]);
119
129
  }
120
130
 
121
131
  /** Consumer-facing slugs: the manifest id, service keys, skill names and trigger names share it. */
122
132
  export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
123
- const slugSchema = z.string().regex(SLUG_PATTERN).max(64);
133
+ export const SLUG_MAX_LENGTH = 64;
134
+ const slugSchema = z.string().regex(SLUG_PATTERN).max(SLUG_MAX_LENGTH);
124
135
 
125
136
  /** A DNS host name in lower case, the way `URL` reports one: dot-separated labels of letters, digits and inner hyphens. */
126
137
  export const HOSTNAME_PATTERN =
@@ -243,6 +254,92 @@ export const toolConfigByType = {
243
254
  client: z.object({}),
244
255
  };
245
256
 
257
+ /** Every tool type, in the order the database enum lists them; see `TOOL_TYPES`. */
258
+ export const TOOL_TYPE_NAMES = ["http", "embedded", "soap", "native", "client"] as const;
259
+
260
+ /**
261
+ * What each tool type is, so no reader restates it: who answers its calls
262
+ * (`kind`: Cortex for a server tool, the browser for a client one), whether
263
+ * it calls an endpoint, whether a call may wait for the end user's
264
+ * approval, whether an input field may be filled from a source Cortex holds
265
+ * (`x-source`) or name its place in the request (`x-target`), whether an
266
+ * author creates one, and the `method` or `readOnly` the type pins whatever
267
+ * the author says. An embedded tool's initiate is never read-only, so a
268
+ * widget reload rejoins its session, and asks no approval, since the widget
269
+ * answers it. SOAP and native tools are POSTed, and a native tool comes from
270
+ * its server's manifest alone, with no field Cortex fills or places. A
271
+ * client tool calls nothing: the client answers it.
272
+ */
273
+ export const TOOL_TYPES = {
274
+ http: {
275
+ kind: "server",
276
+ calls: true,
277
+ approval: true,
278
+ sources: true,
279
+ targets: true,
280
+ creatable: true,
281
+ },
282
+ embedded: {
283
+ kind: "client",
284
+ calls: true,
285
+ approval: false,
286
+ sources: true,
287
+ targets: false,
288
+ creatable: true,
289
+ readOnly: false,
290
+ },
291
+ soap: {
292
+ kind: "server",
293
+ calls: true,
294
+ approval: true,
295
+ sources: true,
296
+ targets: true,
297
+ creatable: true,
298
+ method: "POST",
299
+ },
300
+ native: {
301
+ kind: "server",
302
+ calls: true,
303
+ approval: true,
304
+ sources: false,
305
+ targets: false,
306
+ creatable: false,
307
+ method: "POST",
308
+ },
309
+ client: {
310
+ kind: "client",
311
+ calls: false,
312
+ approval: false,
313
+ sources: false,
314
+ targets: false,
315
+ creatable: true,
316
+ readOnly: true,
317
+ },
318
+ } as const satisfies Record<
319
+ (typeof TOOL_TYPE_NAMES)[number],
320
+ {
321
+ kind: "server" | "client";
322
+ calls: boolean;
323
+ approval: boolean;
324
+ sources: boolean;
325
+ targets: boolean;
326
+ creatable: boolean;
327
+ method?: (typeof TOOL_METHODS)[number];
328
+ readOnly?: boolean;
329
+ }
330
+ >;
331
+
332
+ type CreatableToolType = {
333
+ [TType in keyof typeof TOOL_TYPES]: (typeof TOOL_TYPES)[TType]["creatable"] extends true
334
+ ? TType
335
+ : never;
336
+ }[keyof typeof TOOL_TYPES];
337
+
338
+ /** The types an author creates a tool as, in `TOOL_TYPE_NAMES` order; a native tool comes from its server's manifest. */
339
+ export const CREATABLE_TOOL_TYPES = TOOL_TYPE_NAMES.filter(
340
+ (type): type is CreatableToolType => TOOL_TYPES[type].creatable,
341
+ );
342
+
246
343
  /** A `{name}` parameter in a tool's endpoint URL, filled from the input on each call. */
247
344
  export const ENDPOINT_PARAM_PATTERN = /\{([^{}]+)\}/g;
248
345
 
@@ -261,9 +358,21 @@ export function confinesEndpointParams(url: string) {
261
358
  /** An upstream Cortex calls: http or https. */
262
359
  const upstreamUrlSchema = z.url({ protocol: /^https?$/ });
263
360
 
361
+ /**
362
+ * What a call presents: a header name and the prefix before the value in
363
+ * characters, and how many headers at most (a real upstream wants one or
364
+ * two). The console caps its credential rows at the same.
365
+ */
366
+ export const CREDENTIAL_LIMITS = { headerLength: 120, prefixLength: 40, presentations: 8 } as const;
367
+
264
368
  /** An HTTP header field name (RFC 9110 token). */
265
369
  export const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
266
- const headerNameSchema = z.string().trim().min(1).max(120).regex(HEADER_NAME_PATTERN);
370
+ const headerNameSchema = z
371
+ .string()
372
+ .trim()
373
+ .min(1)
374
+ .max(CREDENTIAL_LIMITS.headerLength)
375
+ .regex(HEADER_NAME_PATTERN);
267
376
 
268
377
  /** Headers Cortex or fetch fills on every request. Lower-cased. */
269
378
  export const transportSetHeaders = ["accept", "content-type", "content-length", "host"] as const;
@@ -292,11 +401,8 @@ export const transportOwnedHeaders = new Set<string>([
292
401
  ...transportForbiddenHeaders,
293
402
  ]);
294
403
 
295
- /** At most this many headers a call presents; a real upstream wants one or two. */
296
- export const CREDENTIAL_PRESENTATION_LIMIT = 8;
297
-
298
404
  /** The index of each entry presenting a header an earlier entry already presents, compared case-insensitively. */
299
- export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
405
+ function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
300
406
  const seen = new Set<string>();
301
407
  return entries.flatMap((entry, index) => {
302
408
  const name = entry.header.toLowerCase();
@@ -306,8 +412,10 @@ export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
306
412
  });
307
413
  }
308
414
 
415
+ /** The longest name Cortex holds a server, model provider, credential or secret under. */
416
+ export const NAME_MAX_LENGTH = 120;
309
417
  /** A server, model provider, credential or secret, by the name Cortex holds it under. */
310
- const nameSchema = z.string().trim().min(1).max(120);
418
+ const nameSchema = z.string().trim().min(1).max(NAME_MAX_LENGTH);
311
419
 
312
420
  const presentedHeaderSchema = headerNameSchema.refine(
313
421
  (name) => !transportOwnedHeaders.has(name.toLowerCase()),
@@ -316,30 +424,45 @@ const presentedHeaderSchema = headerNameSchema.refine(
316
424
 
317
425
  /**
318
426
  * What every call carries: each entry a header, what precedes the value in
319
- * it (`"Bearer "`, say), and the credential by name. A header is presented
320
- * once.
427
+ * it (`"Bearer "`, say), and the credential by name, as `entry` validates
428
+ * it, at most `CREDENTIAL_LIMITS.presentations` of them. A header is
429
+ * presented once; `repeated` words the refusal of a later one.
321
430
  */
322
- function presentationsSchema<TCredential extends string | null>(
323
- header: z.ZodString,
324
- credential: z.ZodType<TCredential, TCredential>,
431
+ export function presentationsSchema<TEntry extends z.ZodType<{ header: string }>>(
432
+ entry: TEntry,
433
+ repeated: () => string,
325
434
  ) {
326
435
  return z
327
- .array(z.strictObject({ header, prefix: z.string().max(40).default(""), credential }))
328
- .max(CREDENTIAL_PRESENTATION_LIMIT)
436
+ .array(entry)
437
+ .max(CREDENTIAL_LIMITS.presentations)
329
438
  .superRefine((entries, context) => {
330
439
  for (const index of repeatedHeaders(entries)) {
331
440
  context.addIssue({
332
441
  code: "custom",
333
442
  path: [index, "header"],
334
- message: "a header presented twice",
443
+ message: repeated(),
335
444
  });
336
445
  }
337
- })
338
- .default([]);
446
+ });
447
+ }
448
+
449
+ /** The presentations a manifest entry declares: no unknown key, an omitted prefix or list empty. */
450
+ function declaredPresentationsSchema<TCredential extends string | null>(
451
+ header: z.ZodString,
452
+ credential: z.ZodType<TCredential, TCredential>,
453
+ ) {
454
+ return presentationsSchema(
455
+ z.strictObject({
456
+ header,
457
+ prefix: z.string().max(CREDENTIAL_LIMITS.prefixLength).default(""),
458
+ credential,
459
+ }),
460
+ () => "a header presented twice",
461
+ ).default([]);
339
462
  }
340
463
 
341
464
  /** A credential, or `null` for the run's own token (the end user's in a chat, the trigger's in a firing). */
342
- const credentialsSchema = presentationsSchema(presentedHeaderSchema, nameSchema.nullable());
465
+ const credentialsSchema = declaredPresentationsSchema(presentedHeaderSchema, nameSchema.nullable());
343
466
 
344
467
  /**
345
468
  * Where Cortex fills a top-level scalar input field from, out of the model's
@@ -348,7 +471,7 @@ const credentialsSchema = presentationsSchema(presentedHeaderSchema, nameSchema.
348
471
  */
349
472
  export const fieldSourceSchema = z.discriminatedUnion("kind", [
350
473
  z.object({ kind: z.literal("variable"), name: z.string().trim().min(1).max(64) }),
351
- z.object({ kind: z.literal("secret"), name: z.string().trim().min(1).max(120) }),
474
+ z.object({ kind: z.literal("secret"), name: nameSchema }),
352
475
  z.object({
353
476
  kind: z.literal("constant"),
354
477
  value: z.union([z.string().max(4_000), z.number(), z.boolean()]),
@@ -363,7 +486,7 @@ export type FieldSource = z.infer<typeof fieldSourceSchema>;
363
486
  * string only, and a constant must be a value of the field's type. Whether a
364
487
  * variable is declared is the agent's server's to say.
365
488
  */
366
- export function sourceFits(
489
+ function sourceFits(
367
490
  source: FieldSource,
368
491
  field: { type: unknown; enum?: ReadonlyArray<string | null> },
369
492
  ) {
@@ -426,16 +549,26 @@ export const fieldTargetSchema = z.discriminatedUnion("kind", [
426
549
  ]);
427
550
  export type FieldTarget = z.infer<typeof fieldTargetSchema>;
428
551
 
429
- const SCALAR_TYPES = ["string", "number", "integer", "boolean"] as const;
552
+ /** The types a field holds one value of. */
553
+ export const SCALAR_TYPES = ["string", "number", "integer", "boolean"] as const;
430
554
  const schemaTypeSchema = z.enum([...SCALAR_TYPES, "object", "array"]);
431
555
 
556
+ /** A field's name and description in characters, and the values its enum lists. The console caps its field rows at the same. */
557
+ export const FIELD_LIMITS = { nameLength: 120, descriptionLength: 500, enumValues: 50 } as const;
558
+
432
559
  /** Untrimmed: two names that differ only in surrounding spaces would collapse into one. */
433
560
  const propertyNameSchema = z
434
561
  .string()
435
562
  .min(1)
436
- .max(120)
563
+ .max(FIELD_LIMITS.nameLength)
437
564
  .refine((name) => name === name.trim(), "no surrounding spaces");
438
565
 
566
+ /** The order an object's properties are shown in, each named once. */
567
+ const propertyOrderSchema = z
568
+ .array(z.string())
569
+ .refine((names) => new Set(names).size === names.length, "a name repeats")
570
+ .optional();
571
+
439
572
  /** What an array holds: a scalar, or an object with properties of its own. */
440
573
  const itemsSchema = z.strictObject({
441
574
  type: z.enum([...SCALAR_TYPES, "object"]),
@@ -443,29 +576,32 @@ const itemsSchema = z.strictObject({
443
576
  return z.record(propertyNameSchema, schemaPropertySchema).optional();
444
577
  },
445
578
  required: z.array(z.string()).optional(),
579
+ "x-order": propertyOrderSchema,
446
580
  });
447
581
 
448
582
  /**
449
583
  * A JSON Schema property, in the part of JSON Schema a field editor holds:
450
584
  * a type (with `"null"` beside it when the value may be null), an `enum` of
451
- * strings for a string, an object's `properties` and `required`, an
452
- * array's `items`, a `description`, and the extensions `x-sensitive`
453
- * (redacted from logs), `x-source` (top-level scalar fields of a tool's
454
- * input only) and `x-target` (top-level fields of a tool's input only).
585
+ * strings for a string, an object's `properties`, `required` and the
586
+ * `x-order` they are shown in, an array's `items`, a `description`, and
587
+ * the extensions `x-sensitive` (redacted from logs), `x-source` (top-level
588
+ * scalar fields of a tool's input only) and `x-target` (top-level fields of
589
+ * a tool's input only).
455
590
  */
456
591
  const schemaPropertySchema = z.strictObject({
457
592
  type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
458
- description: z.string().trim().max(500).optional(),
593
+ description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
459
594
  /** A nullable enum lists `null` beside its values, as the console stores one. */
460
595
  enum: z
461
596
  .array(z.union([z.string().trim().min(1), z.null()]))
462
597
  .min(1)
463
- .max(51)
598
+ .max(FIELD_LIMITS.enumValues + 1)
464
599
  .optional(),
465
600
  get properties() {
466
601
  return z.record(propertyNameSchema, schemaPropertySchema).optional();
467
602
  },
468
603
  required: z.array(z.string()).optional(),
604
+ "x-order": propertyOrderSchema,
469
605
  get items() {
470
606
  return itemsSchema.optional();
471
607
  },
@@ -483,24 +619,30 @@ function typeOf(node: { type: SchemaProperty["type"] }) {
483
619
  /** A refusal below a node: where, relative to it, and why. */
484
620
  type RowIssue = { at: Array<string | number>; message: string };
485
621
 
486
- /** What a node's own keywords break: `properties` or `required` off an object, or `required` naming no property. */
487
- function nodeIssues(node: {
622
+ /** A node holding properties: a property, an array's items, or an input or output schema. */
623
+ type ObjectNode = {
488
624
  type: SchemaProperty["type"];
489
625
  properties?: Record<string, SchemaProperty>;
490
626
  required?: string[];
491
- }) {
627
+ "x-order"?: string[];
628
+ };
629
+
630
+ /** What a node's own keywords break: `properties`, `required` or `x-order` off an object, or the last two naming no property. */
631
+ function nodeIssues(node: ObjectNode) {
492
632
  const issues = new Array<RowIssue>();
493
633
  const object = typeOf(node) === "object";
494
- for (const keyword of ["properties", "required"] as const) {
634
+ for (const keyword of ["properties", "required", "x-order"] as const) {
495
635
  if (!object && node[keyword] !== undefined) {
496
636
  issues.push({ at: [keyword], message: "only an object has it" });
497
637
  }
498
638
  }
499
- node.required?.forEach((name, index) => {
500
- if (!Object.hasOwn(node.properties ?? {}, name)) {
501
- issues.push({ at: ["required", index], message: "names no property" });
502
- }
503
- });
639
+ for (const keyword of ["required", "x-order"] as const) {
640
+ node[keyword]?.forEach((name, index) => {
641
+ if (!Object.hasOwn(node.properties ?? {}, name)) {
642
+ issues.push({ at: [keyword, index], message: "names no property" });
643
+ }
644
+ });
645
+ }
504
646
  return issues;
505
647
  }
506
648
 
@@ -513,20 +655,24 @@ function enumIssueOf(property: SchemaProperty, type: string | undefined) {
513
655
  if (property.enum === undefined) return undefined;
514
656
  if (type !== "string") return "an enum lists a string's values";
515
657
  const values = property.enum.filter((value) => value !== null);
516
- if (values.length === 0 || values.length > 50) return "an enum lists 1 to 50 values";
658
+ if (values.length === 0 || values.length > FIELD_LIMITS.enumValues) {
659
+ return `an enum lists 1 to ${FIELD_LIMITS.enumValues} values`;
660
+ }
517
661
  const nullable = Array.isArray(property.type);
518
662
  if (nullable === property.enum.includes(null)) return undefined;
519
663
  return nullable ? "a nullable enum lists null" : "only a nullable enum lists null";
520
664
  }
521
665
 
666
+ /** The extensions a tool's input binds a field with: a source filling it, a place in the request. */
667
+ type Binding = "x-source" | "x-target";
668
+
522
669
  /**
523
670
  * What one property breaks of itself: an `enum` off a string, with no value,
524
671
  * or listing `null` when the type does not allow it or the other way round;
525
- * `items` off an array or an array without them; `x-source` off a scalar
526
- * of a `bindable` node, or a source that does not fit the field; and
527
- * `x-target` off a `bindable` node.
672
+ * `items` off an array or an array without them; and what its bindings
673
+ * break (`bindingIssues`).
528
674
  */
529
- function propertyIssues(property: SchemaProperty, bindable: boolean) {
675
+ function propertyIssues(property: SchemaProperty, bindings: ReadonlyArray<Binding> | undefined) {
530
676
  const issues = new Array<RowIssue>();
531
677
  const type = typeOf(property);
532
678
  const enumIssue = enumIssueOf(property, type);
@@ -537,21 +683,41 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
537
683
  message: type === "array" ? "an array declares its items" : "only an array has it",
538
684
  });
539
685
  }
540
- const binds = bindable && SCALAR_TYPES.some((scalar) => scalar === type);
541
- if (property["x-source"] !== undefined && !binds) {
542
- issues.push({
543
- at: ["x-source"],
544
- message: "only a top-level scalar field of a tool's input has it",
545
- });
546
- }
547
- if (property["x-target"] !== undefined && !bindable) {
686
+ return [...issues, ...bindingIssues(property, type, bindings)];
687
+ }
688
+
689
+ /**
690
+ * What a property's bindings break: one other than `bindings` directly under
691
+ * a tool's input (none anywhere else), an `x-source` off a scalar, or a
692
+ * source that does not fit the field.
693
+ */
694
+ function bindingIssues(
695
+ property: SchemaProperty,
696
+ type: string | undefined,
697
+ bindings: ReadonlyArray<Binding> | undefined,
698
+ ) {
699
+ const issues = new Array<RowIssue>();
700
+ const scalar = SCALAR_TYPES.some((scalar) => scalar === type);
701
+ // A source fills a scalar; a target places a field of any type.
702
+ const fields = { "x-source": "a top-level scalar field", "x-target": "a top-level field" };
703
+ for (const binding of ["x-source", "x-target"] as const) {
704
+ if (property[binding] === undefined) continue;
705
+ const reaches = bindings !== undefined && (binding === "x-target" || scalar);
706
+ if (reaches && bindings.includes(binding)) continue;
548
707
  issues.push({
549
- at: ["x-target"],
550
- message: "only a top-level field of a tool's input has it",
708
+ at: [binding],
709
+ message: reaches
710
+ ? "this type of tool takes none"
711
+ : `only ${fields[binding]} of a tool's input has it`,
551
712
  });
552
713
  }
553
714
  const source = property["x-source"];
554
- if (binds && source && !sourceFits(source, { type, enum: property.enum })) {
715
+ if (
716
+ scalar &&
717
+ bindings?.includes("x-source") &&
718
+ source &&
719
+ !sourceFits(source, { type, enum: property.enum })
720
+ ) {
555
721
  issues.push({ at: ["x-source"], message: "the source does not fit the field's type" });
556
722
  }
557
723
  return issues;
@@ -560,18 +726,15 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
560
726
  /**
561
727
  * Refuses, at every depth below `path`, what the console's field rows cannot
562
728
  * hold, since it stores a schema as rows and would drop or change it in
563
- * silence (`nodeIssues`, `propertyIssues`); `x-source` only on a scalar,
564
- * and `x-target` only on a field, directly under a node where `bindable`.
729
+ * silence (`nodeIssues`, `propertyIssues`); a binding only one of
730
+ * `bindings`, directly under the node they are given for, and `x-source`
731
+ * only on a scalar.
565
732
  */
566
733
  function heldByRows(
567
- node: {
568
- type: SchemaProperty["type"];
569
- properties?: Record<string, SchemaProperty>;
570
- required?: string[];
571
- },
734
+ node: ObjectNode,
572
735
  context: z.RefinementCtx,
573
736
  path: Array<string | number>,
574
- bindable: boolean,
737
+ bindings: ReadonlyArray<Binding> | undefined,
575
738
  ) {
576
739
  const report = (under: Array<string | number>, issues: ReadonlyArray<RowIssue>) => {
577
740
  for (const { at, message } of issues) {
@@ -581,27 +744,29 @@ function heldByRows(
581
744
  report(path, nodeIssues(node));
582
745
  for (const [name, property] of Object.entries(node.properties ?? {})) {
583
746
  const at = [...path, "properties", name];
584
- report(at, propertyIssues(property, bindable));
585
- heldByRows(property, context, at, false);
586
- if (property.items) heldByRows(property.items, context, [...at, "items"], false);
747
+ report(at, propertyIssues(property, bindings));
748
+ heldByRows(property, context, at, undefined);
749
+ if (property.items) heldByRows(property.items, context, [...at, "items"], undefined);
587
750
  }
588
751
  }
589
752
 
590
753
  /**
591
754
  * An object's JSON Schema, the shape of a tool's or trigger's input and
592
- * output. `bindable` admits `x-source` on its top-level scalar fields and
593
- * `x-target` on its top-level fields, as a tool's input has them.
755
+ * output, and of what the console's field rows write. `bindings`, given for
756
+ * a tool's input, are the extensions its top-level fields may carry:
757
+ * `x-source` on a scalar, `x-target` on any field.
594
758
  */
595
- function objectSchemaOf(bindable: boolean) {
759
+ export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
596
760
  return z
597
761
  .strictObject({
598
762
  type: z.literal("object"),
599
763
  properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
600
764
  required: z.array(z.string()).optional(),
765
+ "x-order": propertyOrderSchema,
601
766
  })
602
- .superRefine((schema, context) => heldByRows(schema, context, [], bindable));
767
+ .superRefine((schema, context) => heldByRows(schema, context, [], bindings));
603
768
  }
604
- const objectSchema = objectSchemaOf(false);
769
+ const objectSchema = objectJsonSchemaOf();
605
770
 
606
771
  const EMPTY_OBJECT = { type: "object" } as const satisfies z.input<typeof objectSchema>;
607
772
 
@@ -617,23 +782,44 @@ const audienceSchema = z.union([
617
782
  }),
618
783
  ]);
619
784
 
785
+ /** A model provider's base URL and each model it lists in characters, and how many it lists. The console caps a provider at the same. */
786
+ export const MODEL_PROVIDER_LIMITS = { urlLength: 2_048, modelLength: 200, models: 100 } as const;
787
+
620
788
  /** A model a provider lists, and the reasoning effort sent with it. */
621
789
  const modelSchema = z.strictObject({
622
790
  provider: nameSchema,
623
- model: z.string().trim().min(1).max(200),
791
+ model: z.string().trim().min(1).max(MODEL_PROVIDER_LIMITS.modelLength),
624
792
  effort: z.string().trim().min(1).max(64).optional(),
625
793
  });
626
794
 
795
+ /** A service's name and description, in characters per locale. The console caps its editor at the same lengths. */
796
+ export const SERVICE_LIMITS = { nameLength: 120, descriptionLength: 4_000 } as const;
797
+
627
798
  /** One line per locale, trimmed. */
628
- const localizedLineSchema = z.record(localeSchema, z.string().trim().min(1).max(120));
629
- const localizedTextSchema = z.record(localeSchema, z.string().trim().min(1).max(4_000));
799
+ const localizedLineSchema = z.record(
800
+ localeSchema,
801
+ z.string().trim().min(1).max(SERVICE_LIMITS.nameLength),
802
+ );
803
+ const localizedTextSchema = z.record(
804
+ localeSchema,
805
+ z.string().trim().min(1).max(SERVICE_LIMITS.descriptionLength),
806
+ );
630
807
 
631
808
  const { tool: toolBounds } = SETTING_BOUNDS;
632
809
 
810
+ /** A `type` tool's input schema, its fields bound only as `TOOL_TYPES` lets the type: filled from a source, placed in the request. */
811
+ function toolInputSchemaOf(type: (typeof TOOL_TYPE_NAMES)[number]) {
812
+ const { sources, targets } = TOOL_TYPES[type];
813
+ return objectJsonSchemaOf([
814
+ ...(sources ? ["x-source" as const] : []),
815
+ ...(targets ? ["x-target" as const] : []),
816
+ ]).default(EMPTY_OBJECT);
817
+ }
818
+
633
819
  /** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, credentials, approval or bound fields. */
634
820
  const toolBaseSchema = z.strictObject({
635
821
  ...serverToolSchema.pick({ name: true, description: true }).shape,
636
- inputSchema: objectSchema.default(EMPTY_OBJECT),
822
+ inputSchema: toolInputSchemaOf("client"),
637
823
  outputSchema: objectSchema.default(EMPTY_OBJECT),
638
824
  timeoutMs: setting(toolBounds.timeoutMs),
639
825
  maxResponseBytes: setting(toolBounds.maxResponseBytes),
@@ -658,7 +844,6 @@ const toolBaseSchema = z.strictObject({
658
844
 
659
845
  /** A tool that calls an endpoint; with `requiresApproval`, every call waits for the end user to approve it. */
660
846
  const calledToolSchema = toolBaseSchema.extend({
661
- inputSchema: objectSchemaOf(true).default(EMPTY_OBJECT),
662
847
  url: upstreamUrlSchema.refine(
663
848
  confinesEndpointParams,
664
849
  "a {parameter} outside the path or query",
@@ -671,28 +856,33 @@ const manifestToolSchema = z
671
856
  .discriminatedUnion("type", [
672
857
  calledToolSchema.extend({
673
858
  type: z.literal("http"),
859
+ inputSchema: toolInputSchemaOf("http"),
674
860
  method: z.enum(TOOL_METHODS),
675
861
  readOnly: z.boolean(),
676
862
  ...toolConfigByType.http.shape,
677
863
  }),
678
- /** The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it. */
679
- calledToolSchema.extend({
864
+ /**
865
+ * The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it.
866
+ * No `requiresApproval`: the widget answers the call, so nothing would ask for it. No field
867
+ * names its place in the request (`x-target`).
868
+ */
869
+ calledToolSchema.omit({ requiresApproval: true }).extend({
680
870
  type: z.literal("embedded"),
871
+ inputSchema: toolInputSchemaOf("embedded"),
681
872
  method: z.enum(TOOL_METHODS),
682
873
  ...toolConfigByType.embedded.shape,
683
874
  }),
684
875
  /** Always POSTed. */
685
876
  calledToolSchema.extend({
686
877
  type: z.literal("soap"),
878
+ inputSchema: toolInputSchemaOf("soap"),
687
879
  readOnly: z.boolean(),
688
880
  ...toolConfigByType.soap.shape,
689
881
  }),
690
882
  /** Answered by your app: the model calls it and the widget hands the call over. */
691
883
  toolBaseSchema.extend({ type: z.literal("client") }),
692
884
  ])
693
- .superRefine((tool, context) => {
694
- if (tool.type !== "client") refuseBlockedApprovalTitle(tool, context, []);
695
- });
885
+ .superRefine((tool, context) => refuseBlockedApprovalTitle(tool, context, []));
696
886
  export type ManifestTool = z.input<typeof manifestToolSchema>;
697
887
 
698
888
  const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]);
@@ -738,9 +928,12 @@ const manifestServiceSchema = z.strictObject({
738
928
  });
739
929
  export type ManifestService = z.input<typeof manifestServiceSchema>;
740
930
 
931
+ /** A skill's description in characters. The console caps its editor at the same. */
932
+ export const SKILL_LIMITS = { descriptionLength: 500 } as const;
933
+
741
934
  const manifestSkillSchema = z.strictObject({
742
935
  name: slugSchema,
743
- description: z.string().trim().min(1).max(500),
936
+ description: z.string().trim().min(1).max(SKILL_LIMITS.descriptionLength),
744
937
  /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}`. */
745
938
  body: z.string().trim().min(1),
746
939
  /** The key of the service the skill renders inside; fixed once created. */
@@ -767,11 +960,14 @@ export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
767
960
  ]);
768
961
  export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
769
962
 
963
+ /** A trigger's description and instructions in characters. The console caps its editor at the same. */
964
+ export const TRIGGER_LIMITS = { descriptionLength: 500, instructionsLength: 20_000 } as const;
965
+
770
966
  /** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
771
967
  const manifestTriggerSchema = z.strictObject({
772
968
  /** `message` is the built-in trigger every agent has. */
773
969
  name: slugSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
774
- description: z.string().trim().min(1).max(500),
970
+ description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
775
971
  inputSchema: objectSchema.default(EMPTY_OBJECT),
776
972
  /** The shape the run answers in; absent, the run answers in free text. */
777
973
  outputSchema: objectSchema
@@ -781,7 +977,7 @@ const manifestTriggerSchema = z.strictObject({
781
977
  )
782
978
  .optional(),
783
979
  /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
784
- instructions: z.string().trim().min(1).max(20_000),
980
+ instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
785
981
  threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
786
982
  /** Firings a day per agent; absent, no limit. */
787
983
  budgetPerDay: z.number().int().positive().optional(),
@@ -853,9 +1049,9 @@ export const mcpToolNamesSchema = z
853
1049
 
854
1050
  const manifestMcpServerSchema = z.strictObject({
855
1051
  /** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
856
- name: z.string().regex(IDENTIFIER_PATTERN).max(64),
1052
+ name: identifierSchema,
857
1053
  url: upstreamUrlSchema,
858
- credentials: presentationsSchema(
1054
+ credentials: declaredPresentationsSchema(
859
1055
  presentedHeaderSchema.refine(
860
1056
  (name) => !mcpTransportHeaders.has(name.toLowerCase()),
861
1057
  "a header the MCP transport sets",
@@ -880,7 +1076,7 @@ const manifestWebhookSchema = z.strictObject({
880
1076
  description: z.string().trim().max(500).default(""),
881
1077
  event: z.enum(SERVER_EVENTS),
882
1078
  url: upstreamUrlSchema,
883
- credentials: presentationsSchema(
1079
+ credentials: declaredPresentationsSchema(
884
1080
  presentedHeaderSchema.refine(
885
1081
  (name) => name.toLowerCase() !== EVENT_HEADER.toLowerCase(),
886
1082
  "the header that names the event",
@@ -893,10 +1089,13 @@ const manifestWebhookSchema = z.strictObject({
893
1089
  });
894
1090
  export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
895
1091
 
1092
+ /** A collection's name and description in characters. The console caps its editor at the same. */
1093
+ export const COLLECTION_LIMITS = { nameLength: 80, descriptionLength: 2_000 } as const;
1094
+
896
1095
  /** Holds documents, which are uploaded to it apart from the manifest. */
897
1096
  const manifestCollectionSchema = z.strictObject({
898
- name: z.string().trim().min(1).max(80),
899
- description: z.string().trim().max(2_000).default(""),
1097
+ name: z.string().trim().min(1).max(COLLECTION_LIMITS.nameLength),
1098
+ description: z.string().trim().max(COLLECTION_LIMITS.descriptionLength).default(""),
900
1099
  /** The key of the service every document in the collection belongs to. */
901
1100
  service: slugSchema.optional(),
902
1101
  /** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
@@ -916,9 +1115,9 @@ const tokenAudienceSchema = z.string().trim().min(1).max(2048).optional();
916
1115
  * How the agent verifies end-user tokens: against the keys its issuer
917
1116
  * publishes at a JWKS URI, with a shared secret named in Cortex's secret
918
1117
  * store and the one algorithm tokens are signed with, through its server's
919
- * `/verify`, or not at all.
1118
+ * `/verify`, or not at all. Cortex stores it on the agent as written here.
920
1119
  */
921
- const agentAuthSchema = z.discriminatedUnion("kind", [
1120
+ export const agentAuthSchema = z.discriminatedUnion("kind", [
922
1121
  z.strictObject({
923
1122
  kind: z.literal("jwks"),
924
1123
  jwksUri: tlsUrlSchema,
@@ -938,8 +1137,11 @@ const agentAuthSchema = z.discriminatedUnion("kind", [
938
1137
 
939
1138
  const { agent: agentBounds, agentContext, agentLimits } = SETTING_BOUNDS;
940
1139
 
941
- /** How the agent runs; every setting left out keeps its default. */
942
- const agentSettingsSchema = z.strictObject({
1140
+ /**
1141
+ * How the agent runs; every setting left out keeps its default. The
1142
+ * console's settings input requires each of them.
1143
+ */
1144
+ export const agentSettingsSchema = z.strictObject({
943
1145
  /** Lets `searchRegistries` search the knowledge in the collections the agent sees. */
944
1146
  ragEnabled: z.boolean().default(false),
945
1147
  /** Lets `searchRegistries` search the tools the agent sees, beyond the pinned and mentioned ones. */
@@ -957,7 +1159,7 @@ const agentSettingsSchema = z.strictObject({
957
1159
  media: z.array(z.enum(MEDIA_FORMS)).default([...DEFAULT_MEDIA_FORMS]),
958
1160
  /** Off, end users cannot message the agent: only its triggers reach it. */
959
1161
  messageTrigger: z.boolean().default(true),
960
- defaultLocale: localeSchema.default("en"),
1162
+ defaultLocale: localeSchema.default(BASE_LOCALE),
961
1163
  rateLimitRequests: setting(agentBounds.rateLimitRequests),
962
1164
  rateLimitWindowSeconds: setting(agentBounds.rateLimitWindowSeconds),
963
1165
  dailyExecutionQuota: setting(agentBounds.dailyExecutionQuota),
@@ -982,9 +1184,12 @@ const agentSettingsSchema = z.strictObject({
982
1184
  .prefault({}),
983
1185
  });
984
1186
 
1187
+ /** An agent's name in characters. The console caps its editor at the same. */
1188
+ export const AGENT_LIMITS = { nameLength: 120 } as const;
1189
+
985
1190
  const manifestAgentSchema = z.strictObject({
986
1191
  slug: agentSlugSchema,
987
- name: z.string().trim().min(1).max(120),
1192
+ name: z.string().trim().min(1).max(AGENT_LIMITS.nameLength),
988
1193
  /** The server it belongs to, by name; a manifest never creates one. */
989
1194
  server: nameSchema,
990
1195
  /** Rich text: it may mention tools and skills, and read `{{variables}}`. */
@@ -1002,23 +1207,61 @@ const manifestAgentSchema = z.strictObject({
1002
1207
  })
1003
1208
  .default({}),
1004
1209
  settings: agentSettingsSchema.prefault({}),
1005
- suggestions: suggestionsSchema.default({ en: [], ar: [] }),
1006
- greeting: greetingSchema.default({ en: "", ar: "" }),
1210
+ suggestions: suggestionsSchema.default(() => byLocale(() => [])),
1211
+ greeting: greetingSchema.default(() => byLocale(() => "")),
1007
1212
  overridable: overridable(["name", "prompt", "models", "settings", "suggestions", "greeting"]),
1008
1213
  });
1009
1214
  export type ManifestAgent = z.input<typeof manifestAgentSchema>;
1010
1215
 
1011
- /** Every name in a list once, so one entry means one row. */
1012
- function uniqueBy<TEntry>(key: (entry: TEntry) => string) {
1013
- return (entries: ReadonlyArray<TEntry>) => new Set(entries.map(key)).size === entries.length;
1014
- }
1216
+ /**
1217
+ * Every kind of entry a manifest owns a row for, in the order an apply
1218
+ * reports a name another row holds and a kind its actor may not write. An
1219
+ * OpenAPI source is none: its operations become `tools` entries.
1220
+ */
1221
+ export const MANIFEST_KINDS = [
1222
+ "agents",
1223
+ "services",
1224
+ "tools",
1225
+ "skills",
1226
+ "triggers",
1227
+ "datasetHosts",
1228
+ "mcpServers",
1229
+ "webhooks",
1230
+ "collections",
1231
+ ] as const;
1232
+ export type ManifestKind = (typeof MANIFEST_KINDS)[number];
1015
1233
 
1016
- function listOf<TEntry extends z.ZodType>(entry: TEntry, key: (value: z.output<TEntry>) => string) {
1017
- return z.array(entry).refine(uniqueBy(key), "a name repeats").default([]);
1234
+ /**
1235
+ * The field each kind's entries go by, which its row's name holds: an
1236
+ * agent's slug, a service's key, any other entry's name. A list holds each
1237
+ * once, so one entry means one row.
1238
+ */
1239
+ export const MANIFEST_NAME_FIELDS = {
1240
+ agents: "slug",
1241
+ tools: "name",
1242
+ services: "key",
1243
+ skills: "name",
1244
+ triggers: "name",
1245
+ datasetHosts: "name",
1246
+ mcpServers: "name",
1247
+ webhooks: "name",
1248
+ collections: "name",
1249
+ } as const satisfies Record<ManifestKind, string>;
1250
+
1251
+ /** A list of entries named by their `field`, refusing a name they repeat. */
1252
+ function listOf<TField extends string, TEntry extends z.ZodType<Record<TField, string>>>(
1253
+ field: TField,
1254
+ entry: TEntry,
1255
+ ) {
1256
+ return z
1257
+ .array(entry)
1258
+ .refine(
1259
+ (entries) => new Set(entries.map((value) => value[field])).size === entries.length,
1260
+ "a name repeats",
1261
+ )
1262
+ .default([]);
1018
1263
  }
1019
1264
 
1020
- const byName = (entry: { name: string }) => entry.name;
1021
-
1022
1265
  /** The id of Cortex's own manifest, which only Cortex applies: the management API refuses any other under it. */
1023
1266
  export const CORTEX_MANIFEST_ID = "cortex";
1024
1267
 
@@ -1032,24 +1275,26 @@ export const CORTEX_MANIFEST_ID = "cortex";
1032
1275
  export const manifestSchema = z.strictObject({
1033
1276
  /** Fixed for the deployment it describes: the rows an apply owns carry it. */
1034
1277
  id: slugSchema,
1035
- agents: listOf(manifestAgentSchema, (agent) => agent.slug),
1036
- tools: listOf(manifestToolSchema, byName),
1037
- openapi: listOf(manifestOpenApiSchema, byName),
1038
- services: listOf(manifestServiceSchema, (service) => service.key),
1039
- skills: listOf(manifestSkillSchema, byName),
1040
- triggers: listOf(manifestTriggerSchema, byName),
1041
- datasetHosts: listOf(manifestDatasetHostSchema, byName),
1042
- mcpServers: listOf(manifestMcpServerSchema, byName),
1043
- webhooks: listOf(manifestWebhookSchema, byName),
1044
- collections: listOf(manifestCollectionSchema, byName),
1045
- });
1278
+ agents: listOf(MANIFEST_NAME_FIELDS.agents, manifestAgentSchema),
1279
+ tools: listOf(MANIFEST_NAME_FIELDS.tools, manifestToolSchema),
1280
+ openapi: listOf("name", manifestOpenApiSchema),
1281
+ services: listOf(MANIFEST_NAME_FIELDS.services, manifestServiceSchema),
1282
+ skills: listOf(MANIFEST_NAME_FIELDS.skills, manifestSkillSchema),
1283
+ triggers: listOf(MANIFEST_NAME_FIELDS.triggers, manifestTriggerSchema),
1284
+ datasetHosts: listOf(MANIFEST_NAME_FIELDS.datasetHosts, manifestDatasetHostSchema),
1285
+ mcpServers: listOf(MANIFEST_NAME_FIELDS.mcpServers, manifestMcpServerSchema),
1286
+ webhooks: listOf(MANIFEST_NAME_FIELDS.webhooks, manifestWebhookSchema),
1287
+ collections: listOf(MANIFEST_NAME_FIELDS.collections, manifestCollectionSchema),
1288
+ } satisfies Record<ManifestKind | "id" | "openapi", z.ZodType>);
1046
1289
  /** A manifest as written: every field with a default may be left out. */
1047
1290
  export type Manifest = z.input<typeof manifestSchema>;
1048
1291
 
1049
- /** The kinds of row a manifest owns; an OpenAPI source's operations are `tools`. */
1050
- const manifestKindSchema = manifestSchema.keyof().exclude(["id", "openapi"]);
1292
+ const manifestKindSchema = z.enum(MANIFEST_KINDS);
1051
1293
 
1052
- /** Entry names per kind: agents by slug, services by key, everything else by name. */
1294
+ /**
1295
+ * Entry names per kind, each by its `MANIFEST_NAME_FIELDS` field. An OpenAPI
1296
+ * source's operations are counted among `tools`.
1297
+ */
1053
1298
  const namesByKindSchema = z.record(manifestKindSchema, z.array(z.string()));
1054
1299
 
1055
1300
  /** What an apply creates, updates and deletes, kind by kind; a preview answers the same without doing it. */