@m6d/cortex-cli 0.15.0 → 0.17.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,20 +62,14 @@ 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: {
68
71
  timeoutMs: { min: 100, max: 120_000, default: 30_000 },
69
72
  maxResponseBytes: { min: 1_024, max: 1_048_576, default: 262_144 },
70
- rateLimitRequests: { min: 1, max: 10_000, default: 60 },
71
- rateLimitWindowSeconds: { min: 1, max: 86_400, default: 60 },
72
- },
73
- agent: {
74
- rateLimitRequests: { min: 1, max: 10_000, default: 60 },
75
- rateLimitWindowSeconds: { min: 1, max: 86_400, default: 60 },
76
- dailyExecutionQuota: { min: 1, max: 1_000_000, default: 10_000 },
77
73
  },
78
74
  /** How much of the conversation the agent's model sees. */
79
75
  agentContext: {
@@ -92,6 +88,18 @@ export const SETTING_BOUNDS = {
92
88
  trigger: { disableAfterFailures: { min: 1, max: 1_000, default: 5 } },
93
89
  datasetHost: { timeoutMs: { min: 100, max: 120_000, default: 30_000 } },
94
90
  webhook: { timeoutMs: { min: 1_000, max: 30_000, default: 10_000 } },
91
+ /** The score, a fraction, a new visitor of an anonymous agent needs to get an id. */
92
+ recaptcha: { minScore: { min: 0.1, max: 0.9, default: 0.5 } },
93
+ /**
94
+ * How often Cortex checks a caller for the server, per minute: a sign-in
95
+ * (a new end-user token it asks the server to verify, or a visitor one
96
+ * of its anonymous agents gives an id), or a trigger's secret. Anyone
97
+ * can try either, so each bounds what strangers cost it.
98
+ */
99
+ server: {
100
+ signInChecksPerMinute: { min: 1, max: 100_000, default: 600 },
101
+ triggerChecksPerMinute: { min: 1, max: 10_000, default: 60 },
102
+ },
95
103
  } as const satisfies Record<string, Record<string, Bounds>>;
96
104
 
97
105
  /** An integer within `bounds`, their default when left out. */
@@ -99,28 +107,90 @@ function setting({ min, max, default: fallback }: Bounds) {
99
107
  return z.number().int().min(min).max(max).default(fallback);
100
108
  }
101
109
 
110
+ /** One number per setting a group of `SETTING_BOUNDS` names. */
111
+ export type SettingValues<TBounds> = { [Name in keyof TBounds]: number };
112
+
113
+ /** Every setting of a group at its default, as a row that sets none stores them. */
114
+ export function settingDefaults<TBounds extends Record<string, Bounds>>(bounds: TBounds) {
115
+ return Object.fromEntries(
116
+ Object.entries(bounds).map(([name, bound]) => [name, bound.default]),
117
+ ) as SettingValues<TBounds>;
118
+ }
119
+
120
+ type RateLimitBounds = { max: number; allUsers: number | null; perUser: number | null };
121
+
122
+ /**
123
+ * Every rate limit an author sets, in the console and in a manifest alike,
124
+ * per minute except an agent's tokens, per UTC day. Each holds for all of an
125
+ * entry's users together and for each user alone, from 1 to `max`, or not
126
+ * at all (null); a manifest that leaves one out gets the default.
127
+ */
128
+ export const RATE_LIMITS = {
129
+ agent: {
130
+ messagesPerMinute: { max: 10_000, allUsers: 600, perUser: 20 },
131
+ tokensPerDay: { max: 1_000_000_000_000, allUsers: null, perUser: 1_000_000 },
132
+ },
133
+ tool: { callsPerMinute: { max: 10_000, allUsers: 60, perUser: 20 } },
134
+ datasetHost: { queriesPerMinute: { max: 10_000, allUsers: 60, perUser: 20 } },
135
+ } as const satisfies Record<string, Record<string, RateLimitBounds>>;
136
+
137
+ /** Whom a rate limit holds for: all of an entry's users together, or each user alone. */
138
+ export const RATE_LIMIT_USERS = ["allUsers", "perUser"] as const;
139
+ export type RateLimitUsers = (typeof RATE_LIMIT_USERS)[number];
140
+
141
+ /** A group of `RATE_LIMITS`, one value per limit for all users and for each user. */
142
+ export function mapRateLimits<TBounds extends Record<string, RateLimitBounds>, TValue>(
143
+ bounds: TBounds,
144
+ value: (bound: RateLimitBounds, users: RateLimitUsers) => TValue,
145
+ ) {
146
+ return Object.fromEntries(
147
+ RATE_LIMIT_USERS.map((users) => [
148
+ users,
149
+ Object.fromEntries(
150
+ Object.entries(bounds).map(([name, bound]) => [name, value(bound, users)]),
151
+ ),
152
+ ]),
153
+ ) as Record<RateLimitUsers, { [Name in keyof TBounds]: TValue }>;
154
+ }
155
+
156
+ /** An entry's rate limits as its row holds them; null lifts one. */
157
+ export type RateLimits<TBounds> = Record<
158
+ RateLimitUsers,
159
+ { [Name in keyof TBounds]: number | null }
160
+ >;
161
+
162
+ /** Every limit of a group at its default, as a row that sets none stores them. */
163
+ export function rateLimitDefaults<TBounds extends Record<string, RateLimitBounds>>(
164
+ bounds: TBounds,
165
+ ) {
166
+ return mapRateLimits(bounds, (bound, users) => bound[users]) satisfies RateLimits<TBounds>;
167
+ }
168
+
169
+ /** One rate limit as a manifest writes it: null lifts it, and left out it takes its default. */
170
+ function rateLimit(bound: RateLimitBounds, users: RateLimitUsers) {
171
+ return z.number().int().min(1).max(bound.max).nullable().default(bound[users]);
172
+ }
173
+
102
174
  /**
103
175
  * The fields of an entry users may change in Cortex after an apply, each
104
176
  * once, from the ones `fields` allows; none when left out. Every other field
105
177
  * is the manifest's alone. An apply keeps a live change to a listed field
106
178
  * unless the manifest changed that field too (a conflict, see
107
179
  * `manifestApplyInputSchema`). Identity, what is fixed at creation, the
108
- * audience, credentials, and whatever an apply checks against the
109
- * manifest's own value are never offered.
180
+ * audience, and whatever an apply checks against the manifest's own value
181
+ * are never offered.
110
182
  */
111
183
  function overridable<const TField extends string>(fields: readonly [TField, ...TField[]]) {
112
184
  return z
113
185
  .array(z.enum(fields))
114
- .refine(
115
- uniqueBy((field: TField) => field),
116
- "a field repeats",
117
- )
186
+ .refine((listed) => new Set(listed).size === listed.length, "a field repeats")
118
187
  .default([]);
119
188
  }
120
189
 
121
190
  /** Consumer-facing slugs: the manifest id, service keys, skill names and trigger names share it. */
122
191
  export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
123
- const slugSchema = z.string().regex(SLUG_PATTERN).max(64);
192
+ export const SLUG_MAX_LENGTH = 64;
193
+ const slugSchema = z.string().regex(SLUG_PATTERN).max(SLUG_MAX_LENGTH);
124
194
 
125
195
  /** A DNS host name in lower case, the way `URL` reports one: dot-separated labels of letters, digits and inner hyphens. */
126
196
  export const HOSTNAME_PATTERN =
@@ -243,6 +313,92 @@ export const toolConfigByType = {
243
313
  client: z.object({}),
244
314
  };
245
315
 
316
+ /** Every tool type, in the order the database enum lists them; see `TOOL_TYPES`. */
317
+ export const TOOL_TYPE_NAMES = ["http", "embedded", "soap", "native", "client"] as const;
318
+
319
+ /**
320
+ * What each tool type is, so no reader restates it: who answers its calls
321
+ * (`kind`: Cortex for a server tool, the browser for a client one), whether
322
+ * it calls an endpoint, whether a call may wait for the end user's
323
+ * approval, whether an input field may be filled from a source Cortex holds
324
+ * (`x-source`) or name its place in the request (`x-target`), whether an
325
+ * author creates one, and the `method` or `readOnly` the type pins whatever
326
+ * the author says. An embedded tool's initiate is never read-only, so a
327
+ * widget reload rejoins its session, and asks no approval, since the widget
328
+ * answers it. SOAP and native tools are POSTed, and a native tool comes from
329
+ * its server's manifest alone, with no field Cortex fills or places. A
330
+ * client tool calls nothing: the client answers it.
331
+ */
332
+ export const TOOL_TYPES = {
333
+ http: {
334
+ kind: "server",
335
+ calls: true,
336
+ approval: true,
337
+ sources: true,
338
+ targets: true,
339
+ creatable: true,
340
+ },
341
+ embedded: {
342
+ kind: "client",
343
+ calls: true,
344
+ approval: false,
345
+ sources: true,
346
+ targets: false,
347
+ creatable: true,
348
+ readOnly: false,
349
+ },
350
+ soap: {
351
+ kind: "server",
352
+ calls: true,
353
+ approval: true,
354
+ sources: true,
355
+ targets: true,
356
+ creatable: true,
357
+ method: "POST",
358
+ },
359
+ native: {
360
+ kind: "server",
361
+ calls: true,
362
+ approval: true,
363
+ sources: false,
364
+ targets: false,
365
+ creatable: false,
366
+ method: "POST",
367
+ },
368
+ client: {
369
+ kind: "client",
370
+ calls: false,
371
+ approval: false,
372
+ sources: false,
373
+ targets: false,
374
+ creatable: true,
375
+ readOnly: true,
376
+ },
377
+ } as const satisfies Record<
378
+ (typeof TOOL_TYPE_NAMES)[number],
379
+ {
380
+ kind: "server" | "client";
381
+ calls: boolean;
382
+ approval: boolean;
383
+ sources: boolean;
384
+ targets: boolean;
385
+ creatable: boolean;
386
+ method?: (typeof TOOL_METHODS)[number];
387
+ readOnly?: boolean;
388
+ }
389
+ >;
390
+
391
+ type CreatableToolType = {
392
+ [TType in keyof typeof TOOL_TYPES]: (typeof TOOL_TYPES)[TType]["creatable"] extends true
393
+ ? TType
394
+ : never;
395
+ }[keyof typeof TOOL_TYPES];
396
+
397
+ /** The types an author creates a tool as, in `TOOL_TYPE_NAMES` order; a native tool comes from its server's manifest. */
398
+ export const CREATABLE_TOOL_TYPES = TOOL_TYPE_NAMES.filter(
399
+ (type): type is CreatableToolType => TOOL_TYPES[type].creatable,
400
+ );
401
+
246
402
  /** A `{name}` parameter in a tool's endpoint URL, filled from the input on each call. */
247
403
  export const ENDPOINT_PARAM_PATTERN = /\{([^{}]+)\}/g;
248
404
 
@@ -261,9 +417,27 @@ export function confinesEndpointParams(url: string) {
261
417
  /** An upstream Cortex calls: http or https. */
262
418
  const upstreamUrlSchema = z.url({ protocol: /^https?$/ });
263
419
 
420
+ /**
421
+ * The headers a call carries: a header name and the prefix before the value
422
+ * in characters, and how many headers at most. The console caps its header
423
+ * rows at the same.
424
+ */
425
+ export const HEADER_LIMITS = { headerLength: 120, prefixLength: 40, headers: 8 } as const;
426
+
264
427
  /** An HTTP header field name (RFC 9110 token). */
265
428
  export const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
266
- const headerNameSchema = z.string().trim().min(1).max(120).regex(HEADER_NAME_PATTERN);
429
+
430
+ /**
431
+ * A header value fetch sends: visible ASCII, tab, space and obs-text.
432
+ * Control characters, and any character past one byte, make fetch throw.
433
+ */
434
+ export const HEADER_VALUE_PATTERN = /^[\t\x20-\x7e\x80-\xff]*$/;
435
+ const headerNameSchema = z
436
+ .string()
437
+ .trim()
438
+ .min(1)
439
+ .max(HEADER_LIMITS.headerLength)
440
+ .regex(HEADER_NAME_PATTERN);
267
441
 
268
442
  /** Headers Cortex or fetch fills on every request. Lower-cased. */
269
443
  export const transportSetHeaders = ["accept", "content-type", "content-length", "host"] as const;
@@ -279,24 +453,21 @@ export const transportForbiddenHeaders = [
279
453
  "proxy-authorization",
280
454
  ] as const;
281
455
 
282
- /** Headers the MCP transport sets on its own requests: no MCP server's credential travels under one. Lower-cased. */
456
+ /** Headers the MCP transport sets on its own requests: no header an MCP server's entry declares travels under one. Lower-cased. */
283
457
  export const mcpTransportHeaders = new Set<string>([
284
458
  "mcp-session-id",
285
459
  "mcp-protocol-version",
286
460
  "last-event-id",
287
461
  ]);
288
462
 
289
- /** Headers no credential or field may travel under; test lower-cased names. */
463
+ /** Headers no declared header or field may travel under; test lower-cased names. */
290
464
  export const transportOwnedHeaders = new Set<string>([
291
465
  ...transportSetHeaders,
292
466
  ...transportForbiddenHeaders,
293
467
  ]);
294
468
 
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
- /** The index of each entry presenting a header an earlier entry already presents, compared case-insensitively. */
299
- export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
469
+ /** The index of each entry declaring a header an earlier entry already declares, compared case-insensitively. */
470
+ function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
300
471
  const seen = new Set<string>();
301
472
  return entries.flatMap((entry, index) => {
302
473
  const name = entry.header.toLowerCase();
@@ -306,8 +477,10 @@ export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
306
477
  });
307
478
  }
308
479
 
480
+ /** The longest name Cortex holds a server, model provider, credential or secret under. */
481
+ export const NAME_MAX_LENGTH = 120;
309
482
  /** A server, model provider, credential or secret, by the name Cortex holds it under. */
310
- const nameSchema = z.string().trim().min(1).max(120);
483
+ const nameSchema = z.string().trim().min(1).max(NAME_MAX_LENGTH);
311
484
 
312
485
  const presentedHeaderSchema = headerNameSchema.refine(
313
486
  (name) => !transportOwnedHeaders.has(name.toLowerCase()),
@@ -315,31 +488,94 @@ const presentedHeaderSchema = headerNameSchema.refine(
315
488
  );
316
489
 
317
490
  /**
318
- * 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.
491
+ * The headers every call carries: each entry a header, what precedes the
492
+ * value in it (`"Bearer "`, say), and where the value comes from, as `entry`
493
+ * validates it, at most `HEADER_LIMITS.headers` of them. A header is
494
+ * declared once; `repeated` words the refusal of a later one.
321
495
  */
322
- function presentationsSchema<TCredential extends string | null>(
323
- header: z.ZodString,
324
- credential: z.ZodType<TCredential, TCredential>,
496
+ export function headerListSchema<TEntry extends z.ZodType<{ header: string }>>(
497
+ entry: TEntry,
498
+ repeated: () => string,
325
499
  ) {
326
500
  return z
327
- .array(z.strictObject({ header, prefix: z.string().max(40).default(""), credential }))
328
- .max(CREDENTIAL_PRESENTATION_LIMIT)
501
+ .array(entry)
502
+ .max(HEADER_LIMITS.headers)
329
503
  .superRefine((entries, context) => {
330
504
  for (const index of repeatedHeaders(entries)) {
331
505
  context.addIssue({
332
506
  code: "custom",
333
507
  path: [index, "header"],
334
- message: "a header presented twice",
508
+ message: repeated(),
335
509
  });
336
510
  }
337
- })
338
- .default([]);
511
+ });
339
512
  }
340
513
 
341
- /** 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());
514
+ /** A variable by name: one the agent's server declares, or a built-in (`timezone`, `locale`, `utcTime`). */
515
+ const variableSourceSchema = z.object({
516
+ kind: z.literal("variable"),
517
+ name: z.string().trim().min(1).max(64),
518
+ });
519
+
520
+ /** A credential Cortex holds or acquires, by name. */
521
+ const credentialSourceSchema = z.object({ kind: z.literal("credential"), name: nameSchema });
522
+
523
+ /** A value written as is. */
524
+ const constantHeaderSourceSchema = z.object({
525
+ kind: z.literal("constant"),
526
+ value: z.string().max(4_000),
527
+ });
528
+
529
+ /**
530
+ * Where a header's value comes from: a credential, the run's own token (the
531
+ * end user's in a chat, the trigger's in a firing), a variable, or a
532
+ * constant. A run token or a variable needs a run; a call without one (a
533
+ * webhook delivery, a catalog read) presents only credentials and constants.
534
+ */
535
+ export const headerSourceSchema = z.discriminatedUnion("kind", [
536
+ credentialSourceSchema,
537
+ z.object({ kind: z.literal("runToken") }),
538
+ variableSourceSchema,
539
+ constantHeaderSourceSchema,
540
+ ]);
541
+ export type HeaderSource = z.infer<typeof headerSourceSchema>;
542
+
543
+ /** The sources a call without a run can present. */
544
+ export const runlessHeaderSourceSchema = z.discriminatedUnion("kind", [
545
+ credentialSourceSchema,
546
+ constantHeaderSourceSchema,
547
+ ]);
548
+
549
+ /**
550
+ * The headers a manifest entry declares: no unknown key, an omitted prefix
551
+ * or list empty, and a prefix or constant only of what a header can carry
552
+ * (`HEADER_VALUE_PATTERN`).
553
+ */
554
+ function declaredHeadersSchema<TSource extends HeaderSource>(
555
+ header: z.ZodString,
556
+ from: z.ZodType<TSource, TSource>,
557
+ ) {
558
+ return headerListSchema(
559
+ z
560
+ .strictObject({
561
+ header,
562
+ prefix: z
563
+ .string()
564
+ .max(HEADER_LIMITS.prefixLength)
565
+ .regex(HEADER_VALUE_PATTERN, "a prefix a header cannot carry")
566
+ .default(""),
567
+ from,
568
+ })
569
+ .refine(
570
+ (entry) =>
571
+ entry.from.kind !== "constant" || HEADER_VALUE_PATTERN.test(entry.from.value),
572
+ { path: ["from", "value"], message: "a value a header cannot carry" },
573
+ ),
574
+ () => "a header declared twice",
575
+ ).default([]);
576
+ }
577
+
578
+ const headersSchema = declaredHeadersSchema(presentedHeaderSchema, headerSourceSchema);
343
579
 
344
580
  /**
345
581
  * Where Cortex fills a top-level scalar input field from, out of the model's
@@ -347,8 +583,8 @@ const credentialsSchema = presentationsSchema(presentedHeaderSchema, nameSchema.
347
583
  * a constant, or the end user's bearer.
348
584
  */
349
585
  export const fieldSourceSchema = z.discriminatedUnion("kind", [
350
- 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) }),
586
+ variableSourceSchema,
587
+ z.object({ kind: z.literal("secret"), name: nameSchema }),
352
588
  z.object({
353
589
  kind: z.literal("constant"),
354
590
  value: z.union([z.string().max(4_000), z.number(), z.boolean()]),
@@ -363,7 +599,7 @@ export type FieldSource = z.infer<typeof fieldSourceSchema>;
363
599
  * string only, and a constant must be a value of the field's type. Whether a
364
600
  * variable is declared is the agent's server's to say.
365
601
  */
366
- export function sourceFits(
602
+ function sourceFits(
367
603
  source: FieldSource,
368
604
  field: { type: unknown; enum?: ReadonlyArray<string | null> },
369
605
  ) {
@@ -426,16 +662,26 @@ export const fieldTargetSchema = z.discriminatedUnion("kind", [
426
662
  ]);
427
663
  export type FieldTarget = z.infer<typeof fieldTargetSchema>;
428
664
 
429
- const SCALAR_TYPES = ["string", "number", "integer", "boolean"] as const;
665
+ /** The types a field holds one value of. */
666
+ export const SCALAR_TYPES = ["string", "number", "integer", "boolean"] as const;
430
667
  const schemaTypeSchema = z.enum([...SCALAR_TYPES, "object", "array"]);
431
668
 
669
+ /** A field's name and description in characters, and the values its enum lists. The console caps its field rows at the same. */
670
+ export const FIELD_LIMITS = { nameLength: 120, descriptionLength: 500, enumValues: 50 } as const;
671
+
432
672
  /** Untrimmed: two names that differ only in surrounding spaces would collapse into one. */
433
673
  const propertyNameSchema = z
434
674
  .string()
435
675
  .min(1)
436
- .max(120)
676
+ .max(FIELD_LIMITS.nameLength)
437
677
  .refine((name) => name === name.trim(), "no surrounding spaces");
438
678
 
679
+ /** The order an object's properties are shown in, each named once. */
680
+ const propertyOrderSchema = z
681
+ .array(z.string())
682
+ .refine((names) => new Set(names).size === names.length, "a name repeats")
683
+ .optional();
684
+
439
685
  /** What an array holds: a scalar, or an object with properties of its own. */
440
686
  const itemsSchema = z.strictObject({
441
687
  type: z.enum([...SCALAR_TYPES, "object"]),
@@ -443,29 +689,32 @@ const itemsSchema = z.strictObject({
443
689
  return z.record(propertyNameSchema, schemaPropertySchema).optional();
444
690
  },
445
691
  required: z.array(z.string()).optional(),
692
+ "x-order": propertyOrderSchema,
446
693
  });
447
694
 
448
695
  /**
449
696
  * A JSON Schema property, in the part of JSON Schema a field editor holds:
450
697
  * 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).
698
+ * strings for a string, an object's `properties`, `required` and the
699
+ * `x-order` they are shown in, an array's `items`, a `description`, and
700
+ * the extensions `x-sensitive` (redacted from logs), `x-source` (top-level
701
+ * scalar fields of a tool's input only) and `x-target` (top-level fields of
702
+ * a tool's input only).
455
703
  */
456
704
  const schemaPropertySchema = z.strictObject({
457
705
  type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
458
- description: z.string().trim().max(500).optional(),
706
+ description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
459
707
  /** A nullable enum lists `null` beside its values, as the console stores one. */
460
708
  enum: z
461
709
  .array(z.union([z.string().trim().min(1), z.null()]))
462
710
  .min(1)
463
- .max(51)
711
+ .max(FIELD_LIMITS.enumValues + 1)
464
712
  .optional(),
465
713
  get properties() {
466
714
  return z.record(propertyNameSchema, schemaPropertySchema).optional();
467
715
  },
468
716
  required: z.array(z.string()).optional(),
717
+ "x-order": propertyOrderSchema,
469
718
  get items() {
470
719
  return itemsSchema.optional();
471
720
  },
@@ -483,24 +732,30 @@ function typeOf(node: { type: SchemaProperty["type"] }) {
483
732
  /** A refusal below a node: where, relative to it, and why. */
484
733
  type RowIssue = { at: Array<string | number>; message: string };
485
734
 
486
- /** What a node's own keywords break: `properties` or `required` off an object, or `required` naming no property. */
487
- function nodeIssues(node: {
735
+ /** A node holding properties: a property, an array's items, or an input or output schema. */
736
+ type ObjectNode = {
488
737
  type: SchemaProperty["type"];
489
738
  properties?: Record<string, SchemaProperty>;
490
739
  required?: string[];
491
- }) {
740
+ "x-order"?: string[];
741
+ };
742
+
743
+ /** What a node's own keywords break: `properties`, `required` or `x-order` off an object, or the last two naming no property. */
744
+ function nodeIssues(node: ObjectNode) {
492
745
  const issues = new Array<RowIssue>();
493
746
  const object = typeOf(node) === "object";
494
- for (const keyword of ["properties", "required"] as const) {
747
+ for (const keyword of ["properties", "required", "x-order"] as const) {
495
748
  if (!object && node[keyword] !== undefined) {
496
749
  issues.push({ at: [keyword], message: "only an object has it" });
497
750
  }
498
751
  }
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
- });
752
+ for (const keyword of ["required", "x-order"] as const) {
753
+ node[keyword]?.forEach((name, index) => {
754
+ if (!Object.hasOwn(node.properties ?? {}, name)) {
755
+ issues.push({ at: [keyword, index], message: "names no property" });
756
+ }
757
+ });
758
+ }
504
759
  return issues;
505
760
  }
506
761
 
@@ -513,20 +768,24 @@ function enumIssueOf(property: SchemaProperty, type: string | undefined) {
513
768
  if (property.enum === undefined) return undefined;
514
769
  if (type !== "string") return "an enum lists a string's values";
515
770
  const values = property.enum.filter((value) => value !== null);
516
- if (values.length === 0 || values.length > 50) return "an enum lists 1 to 50 values";
771
+ if (values.length === 0 || values.length > FIELD_LIMITS.enumValues) {
772
+ return `an enum lists 1 to ${FIELD_LIMITS.enumValues} values`;
773
+ }
517
774
  const nullable = Array.isArray(property.type);
518
775
  if (nullable === property.enum.includes(null)) return undefined;
519
776
  return nullable ? "a nullable enum lists null" : "only a nullable enum lists null";
520
777
  }
521
778
 
779
+ /** The extensions a tool's input binds a field with: a source filling it, a place in the request. */
780
+ type Binding = "x-source" | "x-target";
781
+
522
782
  /**
523
783
  * What one property breaks of itself: an `enum` off a string, with no value,
524
784
  * 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.
785
+ * `items` off an array or an array without them; and what its bindings
786
+ * break (`bindingIssues`).
528
787
  */
529
- function propertyIssues(property: SchemaProperty, bindable: boolean) {
788
+ function propertyIssues(property: SchemaProperty, bindings: ReadonlyArray<Binding> | undefined) {
530
789
  const issues = new Array<RowIssue>();
531
790
  const type = typeOf(property);
532
791
  const enumIssue = enumIssueOf(property, type);
@@ -537,21 +796,41 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
537
796
  message: type === "array" ? "an array declares its items" : "only an array has it",
538
797
  });
539
798
  }
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) {
799
+ return [...issues, ...bindingIssues(property, type, bindings)];
800
+ }
801
+
802
+ /**
803
+ * What a property's bindings break: one other than `bindings` directly under
804
+ * a tool's input (none anywhere else), an `x-source` off a scalar, or a
805
+ * source that does not fit the field.
806
+ */
807
+ function bindingIssues(
808
+ property: SchemaProperty,
809
+ type: string | undefined,
810
+ bindings: ReadonlyArray<Binding> | undefined,
811
+ ) {
812
+ const issues = new Array<RowIssue>();
813
+ const scalar = SCALAR_TYPES.some((scalar) => scalar === type);
814
+ // A source fills a scalar; a target places a field of any type.
815
+ const fields = { "x-source": "a top-level scalar field", "x-target": "a top-level field" };
816
+ for (const binding of ["x-source", "x-target"] as const) {
817
+ if (property[binding] === undefined) continue;
818
+ const reaches = bindings !== undefined && (binding === "x-target" || scalar);
819
+ if (reaches && bindings.includes(binding)) continue;
548
820
  issues.push({
549
- at: ["x-target"],
550
- message: "only a top-level field of a tool's input has it",
821
+ at: [binding],
822
+ message: reaches
823
+ ? "this type of tool takes none"
824
+ : `only ${fields[binding]} of a tool's input has it`,
551
825
  });
552
826
  }
553
827
  const source = property["x-source"];
554
- if (binds && source && !sourceFits(source, { type, enum: property.enum })) {
828
+ if (
829
+ scalar &&
830
+ bindings?.includes("x-source") &&
831
+ source &&
832
+ !sourceFits(source, { type, enum: property.enum })
833
+ ) {
555
834
  issues.push({ at: ["x-source"], message: "the source does not fit the field's type" });
556
835
  }
557
836
  return issues;
@@ -560,18 +839,15 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
560
839
  /**
561
840
  * Refuses, at every depth below `path`, what the console's field rows cannot
562
841
  * 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`.
842
+ * silence (`nodeIssues`, `propertyIssues`); a binding only one of
843
+ * `bindings`, directly under the node they are given for, and `x-source`
844
+ * only on a scalar.
565
845
  */
566
846
  function heldByRows(
567
- node: {
568
- type: SchemaProperty["type"];
569
- properties?: Record<string, SchemaProperty>;
570
- required?: string[];
571
- },
847
+ node: ObjectNode,
572
848
  context: z.RefinementCtx,
573
849
  path: Array<string | number>,
574
- bindable: boolean,
850
+ bindings: ReadonlyArray<Binding> | undefined,
575
851
  ) {
576
852
  const report = (under: Array<string | number>, issues: ReadonlyArray<RowIssue>) => {
577
853
  for (const { at, message } of issues) {
@@ -581,27 +857,29 @@ function heldByRows(
581
857
  report(path, nodeIssues(node));
582
858
  for (const [name, property] of Object.entries(node.properties ?? {})) {
583
859
  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);
860
+ report(at, propertyIssues(property, bindings));
861
+ heldByRows(property, context, at, undefined);
862
+ if (property.items) heldByRows(property.items, context, [...at, "items"], undefined);
587
863
  }
588
864
  }
589
865
 
590
866
  /**
591
867
  * 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.
868
+ * output, and of what the console's field rows write. `bindings`, given for
869
+ * a tool's input, are the extensions its top-level fields may carry:
870
+ * `x-source` on a scalar, `x-target` on any field.
594
871
  */
595
- function objectSchemaOf(bindable: boolean) {
872
+ export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
596
873
  return z
597
874
  .strictObject({
598
875
  type: z.literal("object"),
599
876
  properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
600
877
  required: z.array(z.string()).optional(),
878
+ "x-order": propertyOrderSchema,
601
879
  })
602
- .superRefine((schema, context) => heldByRows(schema, context, [], bindable));
880
+ .superRefine((schema, context) => heldByRows(schema, context, [], bindings));
603
881
  }
604
- const objectSchema = objectSchemaOf(false);
882
+ const objectSchema = objectJsonSchemaOf();
605
883
 
606
884
  const EMPTY_OBJECT = { type: "object" } as const satisfies z.input<typeof objectSchema>;
607
885
 
@@ -617,82 +895,126 @@ const audienceSchema = z.union([
617
895
  }),
618
896
  ]);
619
897
 
898
+ /** 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. */
899
+ export const MODEL_PROVIDER_LIMITS = { urlLength: 2_048, modelLength: 200, models: 100 } as const;
900
+
620
901
  /** A model a provider lists, and the reasoning effort sent with it. */
621
902
  const modelSchema = z.strictObject({
622
903
  provider: nameSchema,
623
- model: z.string().trim().min(1).max(200),
904
+ model: z.string().trim().min(1).max(MODEL_PROVIDER_LIMITS.modelLength),
624
905
  effort: z.string().trim().min(1).max(64).optional(),
625
906
  });
626
907
 
908
+ /** A service's name and description, in characters per locale. The console caps its editor at the same lengths. */
909
+ export const SERVICE_LIMITS = { nameLength: 120, descriptionLength: 4_000 } as const;
910
+
627
911
  /** 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));
912
+ const localizedLineSchema = z.record(
913
+ localeSchema,
914
+ z.string().trim().min(1).max(SERVICE_LIMITS.nameLength),
915
+ );
916
+ const localizedTextSchema = z.record(
917
+ localeSchema,
918
+ z.string().trim().min(1).max(SERVICE_LIMITS.descriptionLength),
919
+ );
630
920
 
631
921
  const { tool: toolBounds } = SETTING_BOUNDS;
922
+ const { tool: toolRateLimits, datasetHost: datasetHostRateLimits } = RATE_LIMITS;
923
+
924
+ /** A `type` tool's input schema, its fields bound only as `TOOL_TYPES` lets the type: filled from a source, placed in the request. */
925
+ function toolInputSchemaOf(type: (typeof TOOL_TYPE_NAMES)[number]) {
926
+ const { sources, targets } = TOOL_TYPES[type];
927
+ return objectJsonSchemaOf([
928
+ ...(sources ? ["x-source" as const] : []),
929
+ ...(targets ? ["x-target" as const] : []),
930
+ ]).default(EMPTY_OBJECT);
931
+ }
632
932
 
633
- /** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, credentials, approval or bound fields. */
933
+ /** What a live write may change on a manifest's tool, and keep through the next apply. */
934
+ const TOOL_OVERRIDABLES = [
935
+ "description",
936
+ "timeoutMs",
937
+ "maxResponseBytes",
938
+ "rateLimits",
939
+ "pinned",
940
+ "tags",
941
+ ] as const;
942
+
943
+ /** The same for a tool that calls an endpoint, whose headers are its own. */
944
+ const CALLED_TOOL_OVERRIDABLES = [...TOOL_OVERRIDABLES, "headers"] as const;
945
+
946
+ /** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, headers, approval or bound fields. */
634
947
  const toolBaseSchema = z.strictObject({
635
948
  ...serverToolSchema.pick({ name: true, description: true }).shape,
636
- inputSchema: objectSchema.default(EMPTY_OBJECT),
949
+ inputSchema: toolInputSchemaOf("client"),
637
950
  outputSchema: objectSchema.default(EMPTY_OBJECT),
638
951
  timeoutMs: setting(toolBounds.timeoutMs),
639
952
  maxResponseBytes: setting(toolBounds.maxResponseBytes),
640
- rateLimitRequests: setting(toolBounds.rateLimitRequests),
641
- rateLimitWindowSeconds: setting(toolBounds.rateLimitWindowSeconds),
953
+ rateLimits: z
954
+ .strictObject({
955
+ allUsers: z
956
+ .strictObject({
957
+ callsPerMinute: rateLimit(toolRateLimits.callsPerMinute, "allUsers"),
958
+ })
959
+ .prefault({}),
960
+ perUser: z
961
+ .strictObject({
962
+ callsPerMinute: rateLimit(toolRateLimits.callsPerMinute, "perUser"),
963
+ })
964
+ .prefault({}),
965
+ })
966
+ .prefault({}),
642
967
  /** Trims the upstream's answer to what the output schema declares. */
643
968
  projectOutput: z.boolean().default(false),
644
969
  /** Listed in every turn of every agent the tool reaches, whatever the question. */
645
970
  pinned: z.boolean().default(false),
646
971
  tags: toolTagsSchema.default([]),
647
972
  audience: audienceSchema,
648
- overridable: overridable([
649
- "description",
650
- "timeoutMs",
651
- "maxResponseBytes",
652
- "rateLimitRequests",
653
- "rateLimitWindowSeconds",
654
- "pinned",
655
- "tags",
656
- ]),
973
+ overridable: overridable(TOOL_OVERRIDABLES),
657
974
  });
658
975
 
659
976
  /** A tool that calls an endpoint; with `requiresApproval`, every call waits for the end user to approve it. */
660
977
  const calledToolSchema = toolBaseSchema.extend({
661
- inputSchema: objectSchemaOf(true).default(EMPTY_OBJECT),
662
978
  url: upstreamUrlSchema.refine(
663
979
  confinesEndpointParams,
664
980
  "a {parameter} outside the path or query",
665
981
  ),
666
- credentials: credentialsSchema,
982
+ headers: headersSchema,
667
983
  requiresApproval: z.boolean().default(false),
984
+ overridable: overridable(CALLED_TOOL_OVERRIDABLES),
668
985
  });
669
986
 
670
987
  const manifestToolSchema = z
671
988
  .discriminatedUnion("type", [
672
989
  calledToolSchema.extend({
673
990
  type: z.literal("http"),
991
+ inputSchema: toolInputSchemaOf("http"),
674
992
  method: z.enum(TOOL_METHODS),
675
993
  readOnly: z.boolean(),
676
994
  ...toolConfigByType.http.shape,
677
995
  }),
678
- /** The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it. */
679
- calledToolSchema.extend({
996
+ /**
997
+ * The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it.
998
+ * No `requiresApproval`: the widget answers the call, so nothing would ask for it. No field
999
+ * names its place in the request (`x-target`).
1000
+ */
1001
+ calledToolSchema.omit({ requiresApproval: true }).extend({
680
1002
  type: z.literal("embedded"),
1003
+ inputSchema: toolInputSchemaOf("embedded"),
681
1004
  method: z.enum(TOOL_METHODS),
682
1005
  ...toolConfigByType.embedded.shape,
683
1006
  }),
684
1007
  /** Always POSTed. */
685
1008
  calledToolSchema.extend({
686
1009
  type: z.literal("soap"),
1010
+ inputSchema: toolInputSchemaOf("soap"),
687
1011
  readOnly: z.boolean(),
688
1012
  ...toolConfigByType.soap.shape,
689
1013
  }),
690
1014
  /** Answered by your app: the model calls it and the widget hands the call over. */
691
1015
  toolBaseSchema.extend({ type: z.literal("client") }),
692
1016
  ])
693
- .superRefine((tool, context) => {
694
- if (tool.type !== "client") refuseBlockedApprovalTitle(tool, context, []);
695
- });
1017
+ .superRefine((tool, context) => refuseBlockedApprovalTitle(tool, context, []));
696
1018
  export type ManifestTool = z.input<typeof manifestToolSchema>;
697
1019
 
698
1020
  const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]);
@@ -701,16 +1023,16 @@ const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]
701
1023
  * An OpenAPI document whose operations become HTTP tools this manifest owns.
702
1024
  * Each apply reads it, imports every operation Cortex supports, keeps those
703
1025
  * tools in step with the document and deletes the ones it no longer has.
704
- * Every tool presents `credentials`. A GET call runs at once; any other
1026
+ * Every tool carries the source's `headers`. A GET call runs at once; any other
705
1027
  * method waits for the end user's approval, unless `approval` names the
706
1028
  * tool the other way.
707
1029
  */
708
1030
  const manifestOpenApiSchema = z.strictObject({
709
1031
  /** Names the source in what an apply reports. */
710
1032
  name: nameSchema,
711
- /** Where the document is read, with no credentials: OpenAPI 3 or Swagger 2, JSON or YAML. */
1033
+ /** Where the document is read, with no headers: OpenAPI 3 or Swagger 2, JSON or YAML. */
712
1034
  url: upstreamUrlSchema,
713
- credentials: credentialsSchema,
1035
+ headers: headersSchema,
714
1036
  /** Where every operation's successful response keeps its result, when the API wraps each answer. */
715
1037
  resultPath: jsonPointerSchema.optional(),
716
1038
  /** Tools listed in every turn of every agent they reach, whatever the question. */
@@ -723,6 +1045,8 @@ const manifestOpenApiSchema = z.strictObject({
723
1045
  "a tool both never and always needs approval",
724
1046
  ),
725
1047
  audience: audienceSchema,
1048
+ /** What a live write may change on each of the source's tools, and keep through the next apply. */
1049
+ overridable: overridable(CALLED_TOOL_OVERRIDABLES),
726
1050
  });
727
1051
  export type ManifestOpenApi = z.input<typeof manifestOpenApiSchema>;
728
1052
 
@@ -738,9 +1062,12 @@ const manifestServiceSchema = z.strictObject({
738
1062
  });
739
1063
  export type ManifestService = z.input<typeof manifestServiceSchema>;
740
1064
 
1065
+ /** A skill's description in characters. The console caps its editor at the same. */
1066
+ export const SKILL_LIMITS = { descriptionLength: 500 } as const;
1067
+
741
1068
  const manifestSkillSchema = z.strictObject({
742
1069
  name: slugSchema,
743
- description: z.string().trim().min(1).max(500),
1070
+ description: z.string().trim().min(1).max(SKILL_LIMITS.descriptionLength),
744
1071
  /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}`. */
745
1072
  body: z.string().trim().min(1),
746
1073
  /** The key of the service the skill renders inside; fixed once created. */
@@ -767,11 +1094,14 @@ export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
767
1094
  ]);
768
1095
  export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
769
1096
 
1097
+ /** A trigger's description and instructions in characters. The console caps its editor at the same. */
1098
+ export const TRIGGER_LIMITS = { descriptionLength: 500, instructionsLength: 20_000 } as const;
1099
+
770
1100
  /** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
771
1101
  const manifestTriggerSchema = z.strictObject({
772
1102
  /** `message` is the built-in trigger every agent has. */
773
1103
  name: slugSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
774
- description: z.string().trim().min(1).max(500),
1104
+ description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
775
1105
  inputSchema: objectSchema.default(EMPTY_OBJECT),
776
1106
  /** The shape the run answers in; absent, the run answers in free text. */
777
1107
  outputSchema: objectSchema
@@ -781,7 +1111,7 @@ const manifestTriggerSchema = z.strictObject({
781
1111
  )
782
1112
  .optional(),
783
1113
  /** 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),
1114
+ instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
785
1115
  threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
786
1116
  /** Firings a day per agent; absent, no limit. */
787
1117
  budgetPerDay: z.number().int().positive().optional(),
@@ -818,14 +1148,34 @@ const manifestDatasetHostSchema = z
818
1148
  .strictObject({
819
1149
  name: datasetIdentifierSchema,
820
1150
  url: upstreamUrlSchema,
821
- credentials: credentialsSchema,
1151
+ headers: headersSchema,
822
1152
  timeoutMs: setting(SETTING_BOUNDS.datasetHost.timeoutMs),
1153
+ rateLimits: z
1154
+ .strictObject({
1155
+ allUsers: z
1156
+ .strictObject({
1157
+ queriesPerMinute: rateLimit(
1158
+ datasetHostRateLimits.queriesPerMinute,
1159
+ "allUsers",
1160
+ ),
1161
+ })
1162
+ .prefault({}),
1163
+ perUser: z
1164
+ .strictObject({
1165
+ queriesPerMinute: rateLimit(
1166
+ datasetHostRateLimits.queriesPerMinute,
1167
+ "perUser",
1168
+ ),
1169
+ })
1170
+ .prefault({}),
1171
+ })
1172
+ .prefault({}),
823
1173
  /** The datasets the host serves that agents may query. */
824
1174
  expose: z.array(datasetIdentifierSchema).min(1).max(500),
825
1175
  /** Among those, the ones listed in every turn, whatever the question. */
826
1176
  pinned: z.array(datasetIdentifierSchema).max(50).default([]),
827
1177
  audience: audienceSchema,
828
- overridable: overridable(["timeoutMs"]),
1178
+ overridable: overridable(["timeoutMs", "rateLimits", "headers"]),
829
1179
  })
830
1180
  .superRefine((host, context) => {
831
1181
  const unexposed = unexposedPins(host);
@@ -853,50 +1203,53 @@ export const mcpToolNamesSchema = z
853
1203
 
854
1204
  const manifestMcpServerSchema = z.strictObject({
855
1205
  /** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
856
- name: z.string().regex(IDENTIFIER_PATTERN).max(64),
1206
+ name: identifierSchema,
857
1207
  url: upstreamUrlSchema,
858
- credentials: presentationsSchema(
1208
+ headers: declaredHeadersSchema(
859
1209
  presentedHeaderSchema.refine(
860
1210
  (name) => !mcpTransportHeaders.has(name.toLowerCase()),
861
1211
  "a header the MCP transport sets",
862
1212
  ),
863
- nameSchema.nullable(),
1213
+ headerSourceSchema,
864
1214
  ),
865
1215
  /** The server's own tool names to offer; empty offers every tool. */
866
1216
  include: mcpToolNamesSchema.default([]),
867
1217
  exclude: mcpToolNamesSchema.default([]),
868
1218
  audience: audienceSchema,
869
- overridable: overridable(["include", "exclude"]),
1219
+ overridable: overridable(["include", "exclude", "headers"]),
870
1220
  });
871
1221
  export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
872
1222
 
873
1223
  /**
874
1224
  * Delivers its event to a receiver while it is declared. A delivery carries
875
- * no run, so every credential it presents is named, and none under the
876
- * header that names the event.
1225
+ * no run, so its headers come from credentials and constants, and none
1226
+ * travels under the header that names the event.
877
1227
  */
878
1228
  const manifestWebhookSchema = z.strictObject({
879
1229
  name: slugSchema,
880
1230
  description: z.string().trim().max(500).default(""),
881
1231
  event: z.enum(SERVER_EVENTS),
882
1232
  url: upstreamUrlSchema,
883
- credentials: presentationsSchema(
1233
+ headers: declaredHeadersSchema(
884
1234
  presentedHeaderSchema.refine(
885
1235
  (name) => name.toLowerCase() !== EVENT_HEADER.toLowerCase(),
886
1236
  "the header that names the event",
887
1237
  ),
888
- nameSchema,
1238
+ runlessHeaderSourceSchema,
889
1239
  ),
890
1240
  timeoutMs: setting(SETTING_BOUNDS.webhook.timeoutMs),
891
1241
  audience: audienceSchema,
892
- overridable: overridable(["description", "timeoutMs"]),
1242
+ overridable: overridable(["description", "timeoutMs", "headers"]),
893
1243
  });
894
1244
  export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
895
1245
 
1246
+ /** A collection's name and description in characters. The console caps its editor at the same. */
1247
+ export const COLLECTION_LIMITS = { nameLength: 80, descriptionLength: 2_000 } as const;
1248
+
896
1249
  /** Holds documents, which are uploaded to it apart from the manifest. */
897
1250
  const manifestCollectionSchema = z.strictObject({
898
- name: z.string().trim().min(1).max(80),
899
- description: z.string().trim().max(2_000).default(""),
1251
+ name: z.string().trim().min(1).max(COLLECTION_LIMITS.nameLength),
1252
+ description: z.string().trim().max(COLLECTION_LIMITS.descriptionLength).default(""),
900
1253
  /** The key of the service every document in the collection belongs to. */
901
1254
  service: slugSchema.optional(),
902
1255
  /** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
@@ -916,9 +1269,9 @@ const tokenAudienceSchema = z.string().trim().min(1).max(2048).optional();
916
1269
  * How the agent verifies end-user tokens: against the keys its issuer
917
1270
  * publishes at a JWKS URI, with a shared secret named in Cortex's secret
918
1271
  * store and the one algorithm tokens are signed with, through its server's
919
- * `/verify`, or not at all.
1272
+ * `/verify`, or not at all. Cortex stores it on the agent as written here.
920
1273
  */
921
- const agentAuthSchema = z.discriminatedUnion("kind", [
1274
+ export const agentAuthSchema = z.discriminatedUnion("kind", [
922
1275
  z.strictObject({
923
1276
  kind: z.literal("jwks"),
924
1277
  jwksUri: tlsUrlSchema,
@@ -932,14 +1285,38 @@ const agentAuthSchema = z.discriminatedUnion("kind", [
932
1285
  issuer: issuerSchema,
933
1286
  audience: tokenAudienceSchema,
934
1287
  }),
935
- z.strictObject({ kind: z.literal("anonymous") }),
1288
+ /**
1289
+ * Each visitor gets an id of their own from Cortex, kept in their
1290
+ * browser. With `recaptcha`, reCAPTCHA Enterprise scores each new
1291
+ * visitor first, and one below `minScore` gets none.
1292
+ */
1293
+ z.strictObject({
1294
+ kind: z.literal("anonymous"),
1295
+ recaptcha: z
1296
+ .strictObject({
1297
+ siteKey: z.string().trim().min(1).max(200),
1298
+ projectId: z.string().trim().min(1).max(200),
1299
+ /** The secret holding a Google Cloud API key allowed to create assessments. */
1300
+ secret: nameSchema,
1301
+ minScore: z
1302
+ .number()
1303
+ .min(SETTING_BOUNDS.recaptcha.minScore.min)
1304
+ .max(SETTING_BOUNDS.recaptcha.minScore.max)
1305
+ .default(SETTING_BOUNDS.recaptcha.minScore.default),
1306
+ })
1307
+ .optional(),
1308
+ }),
936
1309
  z.strictObject({ kind: z.literal("server") }),
937
1310
  ]);
938
1311
 
939
- const { agent: agentBounds, agentContext, agentLimits } = SETTING_BOUNDS;
1312
+ const { agentContext, agentLimits } = SETTING_BOUNDS;
1313
+ const { agent: agentRateLimits } = RATE_LIMITS;
940
1314
 
941
- /** How the agent runs; every setting left out keeps its default. */
942
- const agentSettingsSchema = z.strictObject({
1315
+ /**
1316
+ * How the agent runs; every setting left out keeps its default. The
1317
+ * console's settings input requires each of them.
1318
+ */
1319
+ export const agentSettingsSchema = z.strictObject({
943
1320
  /** Lets `searchRegistries` search the knowledge in the collections the agent sees. */
944
1321
  ragEnabled: z.boolean().default(false),
945
1322
  /** Lets `searchRegistries` search the tools the agent sees, beyond the pinned and mentioned ones. */
@@ -957,10 +1334,23 @@ const agentSettingsSchema = z.strictObject({
957
1334
  media: z.array(z.enum(MEDIA_FORMS)).default([...DEFAULT_MEDIA_FORMS]),
958
1335
  /** Off, end users cannot message the agent: only its triggers reach it. */
959
1336
  messageTrigger: z.boolean().default(true),
960
- defaultLocale: localeSchema.default("en"),
961
- rateLimitRequests: setting(agentBounds.rateLimitRequests),
962
- rateLimitWindowSeconds: setting(agentBounds.rateLimitWindowSeconds),
963
- dailyExecutionQuota: setting(agentBounds.dailyExecutionQuota),
1337
+ defaultLocale: localeSchema.default(BASE_LOCALE),
1338
+ rateLimits: z
1339
+ .strictObject({
1340
+ allUsers: z
1341
+ .strictObject({
1342
+ messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "allUsers"),
1343
+ tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "allUsers"),
1344
+ })
1345
+ .prefault({}),
1346
+ perUser: z
1347
+ .strictObject({
1348
+ messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "perUser"),
1349
+ tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "perUser"),
1350
+ })
1351
+ .prefault({}),
1352
+ })
1353
+ .prefault({}),
964
1354
  context: z
965
1355
  .strictObject({
966
1356
  maxContextTokens: setting(agentContext.maxContextTokens),
@@ -982,9 +1372,12 @@ const agentSettingsSchema = z.strictObject({
982
1372
  .prefault({}),
983
1373
  });
984
1374
 
1375
+ /** An agent's name in characters. The console caps its editor at the same. */
1376
+ export const AGENT_LIMITS = { nameLength: 120 } as const;
1377
+
985
1378
  const manifestAgentSchema = z.strictObject({
986
1379
  slug: agentSlugSchema,
987
- name: z.string().trim().min(1).max(120),
1380
+ name: z.string().trim().min(1).max(AGENT_LIMITS.nameLength),
988
1381
  /** The server it belongs to, by name; a manifest never creates one. */
989
1382
  server: nameSchema,
990
1383
  /** Rich text: it may mention tools and skills, and read `{{variables}}`. */
@@ -1002,23 +1395,61 @@ const manifestAgentSchema = z.strictObject({
1002
1395
  })
1003
1396
  .default({}),
1004
1397
  settings: agentSettingsSchema.prefault({}),
1005
- suggestions: suggestionsSchema.default({ en: [], ar: [] }),
1006
- greeting: greetingSchema.default({ en: "", ar: "" }),
1398
+ suggestions: suggestionsSchema.default(() => byLocale(() => [])),
1399
+ greeting: greetingSchema.default(() => byLocale(() => "")),
1007
1400
  overridable: overridable(["name", "prompt", "models", "settings", "suggestions", "greeting"]),
1008
1401
  });
1009
1402
  export type ManifestAgent = z.input<typeof manifestAgentSchema>;
1010
1403
 
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
- }
1404
+ /**
1405
+ * Every kind of entry a manifest owns a row for, in the order an apply
1406
+ * reports a name another row holds and a kind its actor may not write. An
1407
+ * OpenAPI source is none: its operations become `tools` entries.
1408
+ */
1409
+ export const MANIFEST_KINDS = [
1410
+ "agents",
1411
+ "services",
1412
+ "tools",
1413
+ "skills",
1414
+ "triggers",
1415
+ "datasetHosts",
1416
+ "mcpServers",
1417
+ "webhooks",
1418
+ "collections",
1419
+ ] as const;
1420
+ export type ManifestKind = (typeof MANIFEST_KINDS)[number];
1015
1421
 
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([]);
1422
+ /**
1423
+ * The field each kind's entries go by, which its row's name holds: an
1424
+ * agent's slug, a service's key, any other entry's name. A list holds each
1425
+ * once, so one entry means one row.
1426
+ */
1427
+ export const MANIFEST_NAME_FIELDS = {
1428
+ agents: "slug",
1429
+ tools: "name",
1430
+ services: "key",
1431
+ skills: "name",
1432
+ triggers: "name",
1433
+ datasetHosts: "name",
1434
+ mcpServers: "name",
1435
+ webhooks: "name",
1436
+ collections: "name",
1437
+ } as const satisfies Record<ManifestKind, string>;
1438
+
1439
+ /** A list of entries named by their `field`, refusing a name they repeat. */
1440
+ function listOf<TField extends string, TEntry extends z.ZodType<Record<TField, string>>>(
1441
+ field: TField,
1442
+ entry: TEntry,
1443
+ ) {
1444
+ return z
1445
+ .array(entry)
1446
+ .refine(
1447
+ (entries) => new Set(entries.map((value) => value[field])).size === entries.length,
1448
+ "a name repeats",
1449
+ )
1450
+ .default([]);
1018
1451
  }
1019
1452
 
1020
- const byName = (entry: { name: string }) => entry.name;
1021
-
1022
1453
  /** The id of Cortex's own manifest, which only Cortex applies: the management API refuses any other under it. */
1023
1454
  export const CORTEX_MANIFEST_ID = "cortex";
1024
1455
 
@@ -1032,24 +1463,26 @@ export const CORTEX_MANIFEST_ID = "cortex";
1032
1463
  export const manifestSchema = z.strictObject({
1033
1464
  /** Fixed for the deployment it describes: the rows an apply owns carry it. */
1034
1465
  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
- });
1466
+ agents: listOf(MANIFEST_NAME_FIELDS.agents, manifestAgentSchema),
1467
+ tools: listOf(MANIFEST_NAME_FIELDS.tools, manifestToolSchema),
1468
+ openapi: listOf("name", manifestOpenApiSchema),
1469
+ services: listOf(MANIFEST_NAME_FIELDS.services, manifestServiceSchema),
1470
+ skills: listOf(MANIFEST_NAME_FIELDS.skills, manifestSkillSchema),
1471
+ triggers: listOf(MANIFEST_NAME_FIELDS.triggers, manifestTriggerSchema),
1472
+ datasetHosts: listOf(MANIFEST_NAME_FIELDS.datasetHosts, manifestDatasetHostSchema),
1473
+ mcpServers: listOf(MANIFEST_NAME_FIELDS.mcpServers, manifestMcpServerSchema),
1474
+ webhooks: listOf(MANIFEST_NAME_FIELDS.webhooks, manifestWebhookSchema),
1475
+ collections: listOf(MANIFEST_NAME_FIELDS.collections, manifestCollectionSchema),
1476
+ } satisfies Record<ManifestKind | "id" | "openapi", z.ZodType>);
1046
1477
  /** A manifest as written: every field with a default may be left out. */
1047
1478
  export type Manifest = z.input<typeof manifestSchema>;
1048
1479
 
1049
- /** The kinds of row a manifest owns; an OpenAPI source's operations are `tools`. */
1050
- const manifestKindSchema = manifestSchema.keyof().exclude(["id", "openapi"]);
1480
+ const manifestKindSchema = z.enum(MANIFEST_KINDS);
1051
1481
 
1052
- /** Entry names per kind: agents by slug, services by key, everything else by name. */
1482
+ /**
1483
+ * Entry names per kind, each by its `MANIFEST_NAME_FIELDS` field. An OpenAPI
1484
+ * source's operations are counted among `tools`.
1485
+ */
1053
1486
  const namesByKindSchema = z.record(manifestKindSchema, z.array(z.string()));
1054
1487
 
1055
1488
  /** What an apply creates, updates and deletes, kind by kind; a preview answers the same without doing it. */