@m6d/cortex-cli 0.14.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,
@@ -46,14 +48,23 @@ export const TOOL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
46
48
  */
47
49
  export const RESPONSE_WRAPS = ["items", "value"] as const;
48
50
 
51
+ /** How OpenAPI serializes a query parameter: `form` (the default), `spaceDelimited`, `pipeDelimited` or `deepObject`. */
52
+ export const QUERY_STYLES = ["form", "spaceDelimited", "pipeDelimited", "deepObject"] as const;
53
+ /** How OpenAPI serializes a path parameter: `simple` (the default), `label` or `matrix`. */
54
+ export const PATH_STYLES = ["simple", "label", "matrix"] as const;
55
+
49
56
  export const SOAP_VERSIONS = ["1.1", "1.2"] as const;
50
57
 
58
+ /** An RFC 6901 pointer into a JSON document, naming a member: `/extra`, `/data/items`. */
59
+ const jsonPointerSchema = z.string().regex(/^\//).max(256);
60
+
51
61
  type Bounds = { min: number; max: number; default: number };
52
62
 
53
63
  /**
54
64
  * The range of every number an author sets, in the console and in a
55
- * manifest alike, and the value a manifest that leaves one out gets. The
56
- * 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.
57
68
  */
58
69
  export const SETTING_BOUNDS = {
59
70
  tool: {
@@ -91,9 +102,36 @@ function setting({ min, max, default: fallback }: Bounds) {
91
102
  return z.number().int().min(min).max(max).default(fallback);
92
103
  }
93
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
+
115
+ /**
116
+ * The fields of an entry users may change in Cortex after an apply, each
117
+ * once, from the ones `fields` allows; none when left out. Every other field
118
+ * is the manifest's alone. An apply keeps a live change to a listed field
119
+ * unless the manifest changed that field too (a conflict, see
120
+ * `manifestApplyInputSchema`). Identity, what is fixed at creation, the
121
+ * audience, credentials, and whatever an apply checks against the
122
+ * manifest's own value are never offered.
123
+ */
124
+ function overridable<const TField extends string>(fields: readonly [TField, ...TField[]]) {
125
+ return z
126
+ .array(z.enum(fields))
127
+ .refine((listed) => new Set(listed).size === listed.length, "a field repeats")
128
+ .default([]);
129
+ }
130
+
94
131
  /** Consumer-facing slugs: the manifest id, service keys, skill names and trigger names share it. */
95
132
  export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
96
- 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);
97
135
 
98
136
  /** A DNS host name in lower case, the way `URL` reports one: dot-separated labels of letters, digits and inner hyphens. */
99
137
  export const HOSTNAME_PATTERN =
@@ -175,6 +213,16 @@ export const soapActionSchema = z.string().regex(/^[^"\\\u0000-\u001f\u007f]*$/)
175
213
  /** An envelope template: XML, so never only whitespace. */
176
214
  const envelopeTemplateSchema = z.string().refine((template) => template.trim().length > 0);
177
215
 
216
+ /**
217
+ * The media types a request body may be sent in besides JSON, the default.
218
+ * Each body field is written as its target says.
219
+ */
220
+ export const FORM_MEDIA_TYPES = [
221
+ "application/x-www-form-urlencoded",
222
+ "multipart/form-data",
223
+ ] as const;
224
+ export type FormMediaType = (typeof FORM_MEDIA_TYPES)[number];
225
+
178
226
  /**
179
227
  * The per-type payload a tool stores and publishes. Soap: the envelope
180
228
  * template is the full request XML with `{{field}}` placeholders bound to
@@ -182,7 +230,12 @@ const envelopeTemplateSchema = z.string().refine((template) => template.trim().l
182
230
  * acquires once per request.
183
231
  */
184
232
  export const toolConfigByType = {
185
- http: z.object({ responseWrap: z.enum(RESPONSE_WRAPS).optional() }),
233
+ http: z.object({
234
+ responseWrap: z.enum(RESPONSE_WRAPS).optional(),
235
+ requestMediaType: z.enum(FORM_MEDIA_TYPES).optional(),
236
+ /** Where a successful response keeps the result, for an API that wraps every answer (`{ success, extra }`); read before `responseWrap`. */
237
+ resultPath: jsonPointerSchema.optional(),
238
+ }),
186
239
  embedded: z.object({ embedOrigin: embedOriginSchema, surface: z.enum(TOOL_SURFACES) }),
187
240
  soap: z.object({
188
241
  soapVersion: z.enum(SOAP_VERSIONS),
@@ -201,6 +254,92 @@ export const toolConfigByType = {
201
254
  client: z.object({}),
202
255
  };
203
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
+
204
343
  /** A `{name}` parameter in a tool's endpoint URL, filled from the input on each call. */
205
344
  export const ENDPOINT_PARAM_PATTERN = /\{([^{}]+)\}/g;
206
345
 
@@ -219,9 +358,21 @@ export function confinesEndpointParams(url: string) {
219
358
  /** An upstream Cortex calls: http or https. */
220
359
  const upstreamUrlSchema = z.url({ protocol: /^https?$/ });
221
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
+
222
368
  /** An HTTP header field name (RFC 9110 token). */
223
369
  export const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
224
- 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);
225
376
 
226
377
  /** Headers Cortex or fetch fills on every request. Lower-cased. */
227
378
  export const transportSetHeaders = ["accept", "content-type", "content-length", "host"] as const;
@@ -250,11 +401,8 @@ export const transportOwnedHeaders = new Set<string>([
250
401
  ...transportForbiddenHeaders,
251
402
  ]);
252
403
 
253
- /** At most this many headers a call presents; a real upstream wants one or two. */
254
- export const CREDENTIAL_PRESENTATION_LIMIT = 8;
255
-
256
404
  /** The index of each entry presenting a header an earlier entry already presents, compared case-insensitively. */
257
- export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
405
+ function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
258
406
  const seen = new Set<string>();
259
407
  return entries.flatMap((entry, index) => {
260
408
  const name = entry.header.toLowerCase();
@@ -264,8 +412,10 @@ export function repeatedHeaders(entries: ReadonlyArray<{ header: string }>) {
264
412
  });
265
413
  }
266
414
 
415
+ /** The longest name Cortex holds a server, model provider, credential or secret under. */
416
+ export const NAME_MAX_LENGTH = 120;
267
417
  /** A server, model provider, credential or secret, by the name Cortex holds it under. */
268
- const nameSchema = z.string().trim().min(1).max(120);
418
+ const nameSchema = z.string().trim().min(1).max(NAME_MAX_LENGTH);
269
419
 
270
420
  const presentedHeaderSchema = headerNameSchema.refine(
271
421
  (name) => !transportOwnedHeaders.has(name.toLowerCase()),
@@ -274,30 +424,45 @@ const presentedHeaderSchema = headerNameSchema.refine(
274
424
 
275
425
  /**
276
426
  * What every call carries: each entry a header, what precedes the value in
277
- * it (`"Bearer "`, say), and the credential by name. A header is presented
278
- * 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.
279
430
  */
280
- function presentationsSchema<TCredential extends string | null>(
281
- header: z.ZodString,
282
- credential: z.ZodType<TCredential, TCredential>,
431
+ export function presentationsSchema<TEntry extends z.ZodType<{ header: string }>>(
432
+ entry: TEntry,
433
+ repeated: () => string,
283
434
  ) {
284
435
  return z
285
- .array(z.strictObject({ header, prefix: z.string().max(40).default(""), credential }))
286
- .max(CREDENTIAL_PRESENTATION_LIMIT)
436
+ .array(entry)
437
+ .max(CREDENTIAL_LIMITS.presentations)
287
438
  .superRefine((entries, context) => {
288
439
  for (const index of repeatedHeaders(entries)) {
289
440
  context.addIssue({
290
441
  code: "custom",
291
442
  path: [index, "header"],
292
- message: "a header presented twice",
443
+ message: repeated(),
293
444
  });
294
445
  }
295
- })
296
- .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([]);
297
462
  }
298
463
 
299
464
  /** A credential, or `null` for the run's own token (the end user's in a chat, the trigger's in a firing). */
300
- const credentialsSchema = presentationsSchema(presentedHeaderSchema, nameSchema.nullable());
465
+ const credentialsSchema = declaredPresentationsSchema(presentedHeaderSchema, nameSchema.nullable());
301
466
 
302
467
  /**
303
468
  * Where Cortex fills a top-level scalar input field from, out of the model's
@@ -306,7 +471,7 @@ const credentialsSchema = presentationsSchema(presentedHeaderSchema, nameSchema.
306
471
  */
307
472
  export const fieldSourceSchema = z.discriminatedUnion("kind", [
308
473
  z.object({ kind: z.literal("variable"), name: z.string().trim().min(1).max(64) }),
309
- z.object({ kind: z.literal("secret"), name: z.string().trim().min(1).max(120) }),
474
+ z.object({ kind: z.literal("secret"), name: nameSchema }),
310
475
  z.object({
311
476
  kind: z.literal("constant"),
312
477
  value: z.union([z.string().max(4_000), z.number(), z.boolean()]),
@@ -321,7 +486,7 @@ export type FieldSource = z.infer<typeof fieldSourceSchema>;
321
486
  * string only, and a constant must be a value of the field's type. Whether a
322
487
  * variable is declared is the agent's server's to say.
323
488
  */
324
- export function sourceFits(
489
+ function sourceFits(
325
490
  source: FieldSource,
326
491
  field: { type: unknown; enum?: ReadonlyArray<string | null> },
327
492
  ) {
@@ -343,30 +508,67 @@ export function sourceFits(
343
508
  }
344
509
 
345
510
  /**
346
- * Where a top-level scalar input field travels in the upstream request;
347
- * absent, path placeholders by name, then the query string on GET and the
348
- * body otherwise. `name` renames a query parameter, header or cookie on the
349
- * wire.
511
+ * Where a top-level input field travels in the upstream request; absent,
512
+ * path placeholders by name, then the query string on GET and the body
513
+ * otherwise. `name` renames a query parameter, header or cookie on the wire,
514
+ * and is the name `matrix` style writes into a path.
515
+ * `style` and `explode` write an array or object value as an OpenAPI
516
+ * parameter declares; left out, OpenAPI's defaults hold (`form` exploded for
517
+ * a query, `simple` unexploded for a path and a header). A cookie joins items
518
+ * with commas. In a form body (the tool's `requestMediaType`), a JSON
519
+ * `contentType` sends the value as JSON text, and `style` and `explode`
520
+ * write it as in a query; a multipart body repeats an array's items as
521
+ * parts, typing an object JSON unless `contentType` says text. A JSON body
522
+ * reads none of them.
350
523
  */
351
524
  export const fieldTargetSchema = z.discriminatedUnion("kind", [
352
- z.object({ kind: z.literal("body") }),
353
- z.object({ kind: z.literal("query"), name: z.string().trim().min(1).max(120).optional() }),
354
- z.object({ kind: z.literal("path") }),
355
- z.object({ kind: z.literal("header"), name: headerNameSchema.optional() }),
356
- z.object({ kind: z.literal("cookie"), name: headerNameSchema.optional() }),
525
+ z.strictObject({
526
+ kind: z.literal("body"),
527
+ contentType: z.enum(["application/json", "text/plain"]).optional(),
528
+ style: z.enum(QUERY_STYLES).optional(),
529
+ explode: z.boolean().optional(),
530
+ }),
531
+ z.strictObject({
532
+ kind: z.literal("query"),
533
+ name: z.string().trim().min(1).max(120).optional(),
534
+ style: z.enum(QUERY_STYLES).optional(),
535
+ explode: z.boolean().optional(),
536
+ }),
537
+ z.strictObject({
538
+ kind: z.literal("path"),
539
+ name: z.string().trim().min(1).max(120).optional(),
540
+ style: z.enum(PATH_STYLES).optional(),
541
+ explode: z.boolean().optional(),
542
+ }),
543
+ z.strictObject({
544
+ kind: z.literal("header"),
545
+ name: headerNameSchema.optional(),
546
+ explode: z.boolean().optional(),
547
+ }),
548
+ z.strictObject({ kind: z.literal("cookie"), name: headerNameSchema.optional() }),
357
549
  ]);
358
550
  export type FieldTarget = z.infer<typeof fieldTargetSchema>;
359
551
 
360
- 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;
361
554
  const schemaTypeSchema = z.enum([...SCALAR_TYPES, "object", "array"]);
362
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
+
363
559
  /** Untrimmed: two names that differ only in surrounding spaces would collapse into one. */
364
560
  const propertyNameSchema = z
365
561
  .string()
366
562
  .min(1)
367
- .max(120)
563
+ .max(FIELD_LIMITS.nameLength)
368
564
  .refine((name) => name === name.trim(), "no surrounding spaces");
369
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
+
370
572
  /** What an array holds: a scalar, or an object with properties of its own. */
371
573
  const itemsSchema = z.strictObject({
372
574
  type: z.enum([...SCALAR_TYPES, "object"]),
@@ -374,29 +576,32 @@ const itemsSchema = z.strictObject({
374
576
  return z.record(propertyNameSchema, schemaPropertySchema).optional();
375
577
  },
376
578
  required: z.array(z.string()).optional(),
579
+ "x-order": propertyOrderSchema,
377
580
  });
378
581
 
379
582
  /**
380
583
  * A JSON Schema property, in the part of JSON Schema a field editor holds:
381
584
  * a type (with `"null"` beside it when the value may be null), an `enum` of
382
- * strings for a string, an object's `properties` and `required`, an
383
- * array's `items`, a `description`, and the extensions `x-sensitive`
384
- * (redacted from logs), `x-source` and `x-target` (top-level scalar fields
385
- * 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).
386
590
  */
387
591
  const schemaPropertySchema = z.strictObject({
388
592
  type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
389
- description: z.string().trim().max(500).optional(),
593
+ description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
390
594
  /** A nullable enum lists `null` beside its values, as the console stores one. */
391
595
  enum: z
392
596
  .array(z.union([z.string().trim().min(1), z.null()]))
393
597
  .min(1)
394
- .max(51)
598
+ .max(FIELD_LIMITS.enumValues + 1)
395
599
  .optional(),
396
600
  get properties() {
397
601
  return z.record(propertyNameSchema, schemaPropertySchema).optional();
398
602
  },
399
603
  required: z.array(z.string()).optional(),
604
+ "x-order": propertyOrderSchema,
400
605
  get items() {
401
606
  return itemsSchema.optional();
402
607
  },
@@ -414,24 +619,30 @@ function typeOf(node: { type: SchemaProperty["type"] }) {
414
619
  /** A refusal below a node: where, relative to it, and why. */
415
620
  type RowIssue = { at: Array<string | number>; message: string };
416
621
 
417
- /** What a node's own keywords break: `properties` or `required` off an object, or `required` naming no property. */
418
- function nodeIssues(node: {
622
+ /** A node holding properties: a property, an array's items, or an input or output schema. */
623
+ type ObjectNode = {
419
624
  type: SchemaProperty["type"];
420
625
  properties?: Record<string, SchemaProperty>;
421
626
  required?: string[];
422
- }) {
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) {
423
632
  const issues = new Array<RowIssue>();
424
633
  const object = typeOf(node) === "object";
425
- for (const keyword of ["properties", "required"] as const) {
634
+ for (const keyword of ["properties", "required", "x-order"] as const) {
426
635
  if (!object && node[keyword] !== undefined) {
427
636
  issues.push({ at: [keyword], message: "only an object has it" });
428
637
  }
429
638
  }
430
- node.required?.forEach((name, index) => {
431
- if (!Object.hasOwn(node.properties ?? {}, name)) {
432
- issues.push({ at: ["required", index], message: "names no property" });
433
- }
434
- });
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
+ }
435
646
  return issues;
436
647
  }
437
648
 
@@ -444,20 +655,24 @@ function enumIssueOf(property: SchemaProperty, type: string | undefined) {
444
655
  if (property.enum === undefined) return undefined;
445
656
  if (type !== "string") return "an enum lists a string's values";
446
657
  const values = property.enum.filter((value) => value !== null);
447
- 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
+ }
448
661
  const nullable = Array.isArray(property.type);
449
662
  if (nullable === property.enum.includes(null)) return undefined;
450
663
  return nullable ? "a nullable enum lists null" : "only a nullable enum lists null";
451
664
  }
452
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
+
453
669
  /**
454
670
  * What one property breaks of itself: an `enum` off a string, with no value,
455
671
  * or listing `null` when the type does not allow it or the other way round;
456
- * `items` off an array or an array without them; and `x-source` or
457
- * `x-target` off a scalar of a `bindable` node, or a source that does not
458
- * fit the field.
672
+ * `items` off an array or an array without them; and what its bindings
673
+ * break (`bindingIssues`).
459
674
  */
460
- function propertyIssues(property: SchemaProperty, bindable: boolean) {
675
+ function propertyIssues(property: SchemaProperty, bindings: ReadonlyArray<Binding> | undefined) {
461
676
  const issues = new Array<RowIssue>();
462
677
  const type = typeOf(property);
463
678
  const enumIssue = enumIssueOf(property, type);
@@ -468,17 +683,41 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
468
683
  message: type === "array" ? "an array declares its items" : "only an array has it",
469
684
  });
470
685
  }
471
- const binds = bindable && SCALAR_TYPES.some((scalar) => scalar === type);
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" };
472
703
  for (const binding of ["x-source", "x-target"] as const) {
473
- if (property[binding] !== undefined && !binds) {
474
- issues.push({
475
- at: [binding],
476
- message: "only a top-level scalar field of a tool's input has it",
477
- });
478
- }
704
+ if (property[binding] === undefined) continue;
705
+ const reaches = bindings !== undefined && (binding === "x-target" || scalar);
706
+ if (reaches && bindings.includes(binding)) continue;
707
+ issues.push({
708
+ at: [binding],
709
+ message: reaches
710
+ ? "this type of tool takes none"
711
+ : `only ${fields[binding]} of a tool's input has it`,
712
+ });
479
713
  }
480
714
  const source = property["x-source"];
481
- 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
+ ) {
482
721
  issues.push({ at: ["x-source"], message: "the source does not fit the field's type" });
483
722
  }
484
723
  return issues;
@@ -487,18 +726,15 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
487
726
  /**
488
727
  * Refuses, at every depth below `path`, what the console's field rows cannot
489
728
  * hold, since it stores a schema as rows and would drop or change it in
490
- * silence (`nodeIssues`, `propertyIssues`); `x-source` and `x-target` only
491
- * on a scalar 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.
492
732
  */
493
733
  function heldByRows(
494
- node: {
495
- type: SchemaProperty["type"];
496
- properties?: Record<string, SchemaProperty>;
497
- required?: string[];
498
- },
734
+ node: ObjectNode,
499
735
  context: z.RefinementCtx,
500
736
  path: Array<string | number>,
501
- bindable: boolean,
737
+ bindings: ReadonlyArray<Binding> | undefined,
502
738
  ) {
503
739
  const report = (under: Array<string | number>, issues: ReadonlyArray<RowIssue>) => {
504
740
  for (const { at, message } of issues) {
@@ -508,27 +744,29 @@ function heldByRows(
508
744
  report(path, nodeIssues(node));
509
745
  for (const [name, property] of Object.entries(node.properties ?? {})) {
510
746
  const at = [...path, "properties", name];
511
- report(at, propertyIssues(property, bindable));
512
- heldByRows(property, context, at, false);
513
- 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);
514
750
  }
515
751
  }
516
752
 
517
753
  /**
518
754
  * An object's JSON Schema, the shape of a tool's or trigger's input and
519
- * output. `bindable` admits `x-source` and `x-target` on its top-level
520
- * scalar 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.
521
758
  */
522
- function objectSchemaOf(bindable: boolean) {
759
+ export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
523
760
  return z
524
761
  .strictObject({
525
762
  type: z.literal("object"),
526
763
  properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
527
764
  required: z.array(z.string()).optional(),
765
+ "x-order": propertyOrderSchema,
528
766
  })
529
- .superRefine((schema, context) => heldByRows(schema, context, [], bindable));
767
+ .superRefine((schema, context) => heldByRows(schema, context, [], bindings));
530
768
  }
531
- const objectSchema = objectSchemaOf(false);
769
+ const objectSchema = objectJsonSchemaOf();
532
770
 
533
771
  const EMPTY_OBJECT = { type: "object" } as const satisfies z.input<typeof objectSchema>;
534
772
 
@@ -544,23 +782,44 @@ const audienceSchema = z.union([
544
782
  }),
545
783
  ]);
546
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
+
547
788
  /** A model a provider lists, and the reasoning effort sent with it. */
548
789
  const modelSchema = z.strictObject({
549
790
  provider: nameSchema,
550
- model: z.string().trim().min(1).max(200),
791
+ model: z.string().trim().min(1).max(MODEL_PROVIDER_LIMITS.modelLength),
551
792
  effort: z.string().trim().min(1).max(64).optional(),
552
793
  });
553
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
+
554
798
  /** One line per locale, trimmed. */
555
- const localizedLineSchema = z.record(localeSchema, z.string().trim().min(1).max(120));
556
- 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
+ );
557
807
 
558
808
  const { tool: toolBounds } = SETTING_BOUNDS;
559
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
+
560
819
  /** Shared by every tool type; a tool that calls nothing (`client`) has no endpoint, credentials, approval or bound fields. */
561
820
  const toolBaseSchema = z.strictObject({
562
821
  ...serverToolSchema.pick({ name: true, description: true }).shape,
563
- inputSchema: objectSchema.default(EMPTY_OBJECT),
822
+ inputSchema: toolInputSchemaOf("client"),
564
823
  outputSchema: objectSchema.default(EMPTY_OBJECT),
565
824
  timeoutMs: setting(toolBounds.timeoutMs),
566
825
  maxResponseBytes: setting(toolBounds.maxResponseBytes),
@@ -572,11 +831,19 @@ const toolBaseSchema = z.strictObject({
572
831
  pinned: z.boolean().default(false),
573
832
  tags: toolTagsSchema.default([]),
574
833
  audience: audienceSchema,
834
+ overridable: overridable([
835
+ "description",
836
+ "timeoutMs",
837
+ "maxResponseBytes",
838
+ "rateLimitRequests",
839
+ "rateLimitWindowSeconds",
840
+ "pinned",
841
+ "tags",
842
+ ]),
575
843
  });
576
844
 
577
845
  /** A tool that calls an endpoint; with `requiresApproval`, every call waits for the end user to approve it. */
578
846
  const calledToolSchema = toolBaseSchema.extend({
579
- inputSchema: objectSchemaOf(true).default(EMPTY_OBJECT),
580
847
  url: upstreamUrlSchema.refine(
581
848
  confinesEndpointParams,
582
849
  "a {parameter} outside the path or query",
@@ -589,28 +856,33 @@ const manifestToolSchema = z
589
856
  .discriminatedUnion("type", [
590
857
  calledToolSchema.extend({
591
858
  type: z.literal("http"),
859
+ inputSchema: toolInputSchemaOf("http"),
592
860
  method: z.enum(TOOL_METHODS),
593
861
  readOnly: z.boolean(),
594
862
  ...toolConfigByType.http.shape,
595
863
  }),
596
- /** The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it. */
597
- 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({
598
870
  type: z.literal("embedded"),
871
+ inputSchema: toolInputSchemaOf("embedded"),
599
872
  method: z.enum(TOOL_METHODS),
600
873
  ...toolConfigByType.embedded.shape,
601
874
  }),
602
875
  /** Always POSTed. */
603
876
  calledToolSchema.extend({
604
877
  type: z.literal("soap"),
878
+ inputSchema: toolInputSchemaOf("soap"),
605
879
  readOnly: z.boolean(),
606
880
  ...toolConfigByType.soap.shape,
607
881
  }),
608
882
  /** Answered by your app: the model calls it and the widget hands the call over. */
609
883
  toolBaseSchema.extend({ type: z.literal("client") }),
610
884
  ])
611
- .superRefine((tool, context) => {
612
- if (tool.type !== "client") refuseBlockedApprovalTitle(tool, context, []);
613
- });
885
+ .superRefine((tool, context) => refuseBlockedApprovalTitle(tool, context, []));
614
886
  export type ManifestTool = z.input<typeof manifestToolSchema>;
615
887
 
616
888
  const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]);
@@ -629,6 +901,8 @@ const manifestOpenApiSchema = z.strictObject({
629
901
  /** Where the document is read, with no credentials: OpenAPI 3 or Swagger 2, JSON or YAML. */
630
902
  url: upstreamUrlSchema,
631
903
  credentials: credentialsSchema,
904
+ /** Where every operation's successful response keeps its result, when the API wraps each answer. */
905
+ resultPath: jsonPointerSchema.optional(),
632
906
  /** Tools listed in every turn of every agent they reach, whatever the question. */
633
907
  pinned: toolNamesSchema,
634
908
  approval: z
@@ -650,17 +924,22 @@ const manifestServiceSchema = z.strictObject({
650
924
  /** Rich text: it may mention tools and skills. */
651
925
  description: localizedTextSchema,
652
926
  audience: audienceSchema,
927
+ overridable: overridable(["description"]),
653
928
  });
654
929
  export type ManifestService = z.input<typeof manifestServiceSchema>;
655
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
+
656
934
  const manifestSkillSchema = z.strictObject({
657
935
  name: slugSchema,
658
- description: z.string().trim().min(1).max(500),
936
+ description: z.string().trim().min(1).max(SKILL_LIMITS.descriptionLength),
659
937
  /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}`. */
660
938
  body: z.string().trim().min(1),
661
939
  /** The key of the service the skill renders inside; fixed once created. */
662
940
  service: slugSchema.optional(),
663
941
  audience: audienceSchema,
942
+ overridable: overridable(["description", "body"]),
664
943
  });
665
944
  export type ManifestSkill = z.input<typeof manifestSkillSchema>;
666
945
 
@@ -677,15 +956,18 @@ export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
677
956
  z.object({ policy: z.literal("single") }),
678
957
  z.object({ policy: z.literal("per-firing") }),
679
958
  z.object({ policy: z.literal("ephemeral") }),
680
- z.object({ policy: z.literal("keyed"), key: z.string().regex(/^\//).max(256) }),
959
+ z.object({ policy: z.literal("keyed"), key: jsonPointerSchema }),
681
960
  ]);
682
961
  export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
683
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
+
684
966
  /** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
685
967
  const manifestTriggerSchema = z.strictObject({
686
968
  /** `message` is the built-in trigger every agent has. */
687
969
  name: slugSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
688
- description: z.string().trim().min(1).max(500),
970
+ description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
689
971
  inputSchema: objectSchema.default(EMPTY_OBJECT),
690
972
  /** The shape the run answers in; absent, the run answers in free text. */
691
973
  outputSchema: objectSchema
@@ -695,7 +977,7 @@ const manifestTriggerSchema = z.strictObject({
695
977
  )
696
978
  .optional(),
697
979
  /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
698
- instructions: z.string().trim().min(1).max(20_000),
980
+ instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
699
981
  threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
700
982
  /** Firings a day per agent; absent, no limit. */
701
983
  budgetPerDay: z.number().int().positive().optional(),
@@ -708,6 +990,14 @@ const manifestTriggerSchema = z.strictObject({
708
990
  /** The model its runs talk to, over each agent's. */
709
991
  model: modelSchema.optional(),
710
992
  audience: audienceSchema,
993
+ overridable: overridable([
994
+ "description",
995
+ "instructions",
996
+ "threadPolicy",
997
+ "budgetPerDay",
998
+ "disableAfterFailures",
999
+ "model",
1000
+ ]),
711
1001
  });
712
1002
  export type ManifestTrigger = z.input<typeof manifestTriggerSchema>;
713
1003
 
@@ -731,6 +1021,7 @@ const manifestDatasetHostSchema = z
731
1021
  /** Among those, the ones listed in every turn, whatever the question. */
732
1022
  pinned: z.array(datasetIdentifierSchema).max(50).default([]),
733
1023
  audience: audienceSchema,
1024
+ overridable: overridable(["timeoutMs"]),
734
1025
  })
735
1026
  .superRefine((host, context) => {
736
1027
  const unexposed = unexposedPins(host);
@@ -758,9 +1049,9 @@ export const mcpToolNamesSchema = z
758
1049
 
759
1050
  const manifestMcpServerSchema = z.strictObject({
760
1051
  /** Prefixes every tool the server contributes: scripts call `tools.<name>_<tool>()`. */
761
- name: z.string().regex(IDENTIFIER_PATTERN).max(64),
1052
+ name: identifierSchema,
762
1053
  url: upstreamUrlSchema,
763
- credentials: presentationsSchema(
1054
+ credentials: declaredPresentationsSchema(
764
1055
  presentedHeaderSchema.refine(
765
1056
  (name) => !mcpTransportHeaders.has(name.toLowerCase()),
766
1057
  "a header the MCP transport sets",
@@ -771,6 +1062,7 @@ const manifestMcpServerSchema = z.strictObject({
771
1062
  include: mcpToolNamesSchema.default([]),
772
1063
  exclude: mcpToolNamesSchema.default([]),
773
1064
  audience: audienceSchema,
1065
+ overridable: overridable(["include", "exclude"]),
774
1066
  });
775
1067
  export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
776
1068
 
@@ -784,7 +1076,7 @@ const manifestWebhookSchema = z.strictObject({
784
1076
  description: z.string().trim().max(500).default(""),
785
1077
  event: z.enum(SERVER_EVENTS),
786
1078
  url: upstreamUrlSchema,
787
- credentials: presentationsSchema(
1079
+ credentials: declaredPresentationsSchema(
788
1080
  presentedHeaderSchema.refine(
789
1081
  (name) => name.toLowerCase() !== EVENT_HEADER.toLowerCase(),
790
1082
  "the header that names the event",
@@ -793,16 +1085,23 @@ const manifestWebhookSchema = z.strictObject({
793
1085
  ),
794
1086
  timeoutMs: setting(SETTING_BOUNDS.webhook.timeoutMs),
795
1087
  audience: audienceSchema,
1088
+ overridable: overridable(["description", "timeoutMs"]),
796
1089
  });
797
1090
  export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
798
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
+
799
1095
  /** Holds documents, which are uploaded to it apart from the manifest. */
800
1096
  const manifestCollectionSchema = z.strictObject({
801
- name: z.string().trim().min(1).max(80),
802
- 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(""),
803
1099
  /** The key of the service every document in the collection belongs to. */
804
1100
  service: slugSchema.optional(),
1101
+ /** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
1102
+ citable: z.boolean().default(true),
805
1103
  audience: audienceSchema,
1104
+ overridable: overridable(["description"]),
806
1105
  });
807
1106
  export type ManifestCollection = z.input<typeof manifestCollectionSchema>;
808
1107
 
@@ -816,9 +1115,9 @@ const tokenAudienceSchema = z.string().trim().min(1).max(2048).optional();
816
1115
  * How the agent verifies end-user tokens: against the keys its issuer
817
1116
  * publishes at a JWKS URI, with a shared secret named in Cortex's secret
818
1117
  * store and the one algorithm tokens are signed with, through its server's
819
- * `/verify`, or not at all.
1118
+ * `/verify`, or not at all. Cortex stores it on the agent as written here.
820
1119
  */
821
- const agentAuthSchema = z.discriminatedUnion("kind", [
1120
+ export const agentAuthSchema = z.discriminatedUnion("kind", [
822
1121
  z.strictObject({
823
1122
  kind: z.literal("jwks"),
824
1123
  jwksUri: tlsUrlSchema,
@@ -838,12 +1137,16 @@ const agentAuthSchema = z.discriminatedUnion("kind", [
838
1137
 
839
1138
  const { agent: agentBounds, agentContext, agentLimits } = SETTING_BOUNDS;
840
1139
 
841
- /** How the agent runs; every setting left out keeps its default. */
842
- const agentSettingsSchema = z.strictObject({
843
- /** Offers `searchKnowledge` over the collections the agent sees. */
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({
1145
+ /** Lets `searchRegistries` search the knowledge in the collections the agent sees. */
844
1146
  ragEnabled: z.boolean().default(false),
845
- /** Offers `searchTools` over the tools the agent sees, beyond the pinned and mentioned ones. */
1147
+ /** Lets `searchRegistries` search the tools the agent sees, beyond the pinned and mentioned ones. */
846
1148
  toolRegistryEnabled: z.boolean().default(false),
1149
+ /** Lets `searchRegistries` search the services the agent sees. */
847
1150
  serviceRegistryEnabled: z.boolean().default(false),
848
1151
  attachmentsEnabled: z.boolean().default(true),
849
1152
  /** Hosts the built-in `fetchImage` tool may pull images from; none, no such tool. */
@@ -856,7 +1159,7 @@ const agentSettingsSchema = z.strictObject({
856
1159
  media: z.array(z.enum(MEDIA_FORMS)).default([...DEFAULT_MEDIA_FORMS]),
857
1160
  /** Off, end users cannot message the agent: only its triggers reach it. */
858
1161
  messageTrigger: z.boolean().default(true),
859
- defaultLocale: localeSchema.default("en"),
1162
+ defaultLocale: localeSchema.default(BASE_LOCALE),
860
1163
  rateLimitRequests: setting(agentBounds.rateLimitRequests),
861
1164
  rateLimitWindowSeconds: setting(agentBounds.rateLimitWindowSeconds),
862
1165
  dailyExecutionQuota: setting(agentBounds.dailyExecutionQuota),
@@ -881,9 +1184,12 @@ const agentSettingsSchema = z.strictObject({
881
1184
  .prefault({}),
882
1185
  });
883
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
+
884
1190
  const manifestAgentSchema = z.strictObject({
885
1191
  slug: agentSlugSchema,
886
- name: z.string().trim().min(1).max(120),
1192
+ name: z.string().trim().min(1).max(AGENT_LIMITS.nameLength),
887
1193
  /** The server it belongs to, by name; a manifest never creates one. */
888
1194
  server: nameSchema,
889
1195
  /** Rich text: it may mention tools and skills, and read `{{variables}}`. */
@@ -901,55 +1207,95 @@ const manifestAgentSchema = z.strictObject({
901
1207
  })
902
1208
  .default({}),
903
1209
  settings: agentSettingsSchema.prefault({}),
904
- suggestions: suggestionsSchema.default({ en: [], ar: [] }),
905
- greeting: greetingSchema.default({ en: "", ar: "" }),
1210
+ suggestions: suggestionsSchema.default(() => byLocale(() => [])),
1211
+ greeting: greetingSchema.default(() => byLocale(() => "")),
1212
+ overridable: overridable(["name", "prompt", "models", "settings", "suggestions", "greeting"]),
906
1213
  });
907
1214
  export type ManifestAgent = z.input<typeof manifestAgentSchema>;
908
1215
 
909
- /** Every name in a list once, so one entry means one row. */
910
- function uniqueBy<TEntry>(key: (entry: TEntry) => string) {
911
- return (entries: ReadonlyArray<TEntry>) => new Set(entries.map(key)).size === entries.length;
912
- }
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];
913
1233
 
914
- function listOf<TEntry extends z.ZodType>(entry: TEntry, key: (value: z.output<TEntry>) => string) {
915
- 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([]);
916
1263
  }
917
1264
 
918
- const byName = (entry: { name: string }) => entry.name;
919
-
920
1265
  /** The id of Cortex's own manifest, which only Cortex applies: the management API refuses any other under it. */
921
1266
  export const CORTEX_MANIFEST_ID = "cortex";
922
1267
 
923
1268
  /**
924
1269
  * A deployment's agents and components, owned by the manifest `id`: an apply
925
1270
  * of it creates what is new, updates what changed, and deletes what it no
926
- * longer lists, touching nothing it did not create.
1271
+ * longer lists, touching nothing it did not create. What an entry lists as
1272
+ * `overridable` may then change in Cortex too, and an apply keeps that
1273
+ * change unless the manifest changed the same field.
927
1274
  */
928
1275
  export const manifestSchema = z.strictObject({
929
1276
  /** Fixed for the deployment it describes: the rows an apply owns carry it. */
930
1277
  id: slugSchema,
931
- agents: listOf(manifestAgentSchema, (agent) => agent.slug),
932
- tools: listOf(manifestToolSchema, byName),
933
- openapi: listOf(manifestOpenApiSchema, byName),
934
- services: listOf(manifestServiceSchema, (service) => service.key),
935
- skills: listOf(manifestSkillSchema, byName),
936
- triggers: listOf(manifestTriggerSchema, byName),
937
- datasetHosts: listOf(manifestDatasetHostSchema, byName),
938
- mcpServers: listOf(manifestMcpServerSchema, byName),
939
- webhooks: listOf(manifestWebhookSchema, byName),
940
- collections: listOf(manifestCollectionSchema, byName),
941
- });
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>);
942
1289
  /** A manifest as written: every field with a default may be left out. */
943
1290
  export type Manifest = z.input<typeof manifestSchema>;
944
1291
 
1292
+ const manifestKindSchema = z.enum(MANIFEST_KINDS);
1293
+
945
1294
  /**
946
- * Entry names per kind: agents by slug, services by key, everything else by
947
- * name. An OpenAPI source's operations are counted among `tools`.
1295
+ * Entry names per kind, each by its `MANIFEST_NAME_FIELDS` field. An OpenAPI
1296
+ * source's operations are counted among `tools`.
948
1297
  */
949
- const namesByKindSchema = z.record(
950
- manifestSchema.keyof().exclude(["id", "openapi"]),
951
- z.array(z.string()),
952
- );
1298
+ const namesByKindSchema = z.record(manifestKindSchema, z.array(z.string()));
953
1299
 
954
1300
  /** What an apply creates, updates and deletes, kind by kind; a preview answers the same without doing it. */
955
1301
  export const manifestChangesSchema = z.object({
@@ -958,6 +1304,32 @@ export const manifestChangesSchema = z.object({
958
1304
  delete: namesByKindSchema,
959
1305
  });
960
1306
 
1307
+ /** A field of one entry that both the manifest and a live write changed since the last apply. */
1308
+ const manifestConflictSchema = z.strictObject({
1309
+ kind: manifestKindSchema,
1310
+ name: z.string(),
1311
+ field: z.string(),
1312
+ });
1313
+
1314
+ /** Which value a conflict keeps: the one changed live in Cortex, or the manifest's. */
1315
+ const conflictSideSchema = z.enum(["live", "manifest"]);
1316
+
1317
+ /**
1318
+ * What preview and sync take: the manifest, and how its conflicts resolve.
1319
+ * A conflict takes its own entry in `resolutions`, else `prefer`; a sync
1320
+ * left with one neither answers is refused as `unresolved_conflicts`,
1321
+ * naming them. A resolution for a field that no longer conflicts is ignored.
1322
+ */
1323
+ export const manifestApplyInputSchema = manifestSchema.extend({
1324
+ prefer: conflictSideSchema.optional(),
1325
+ resolutions: z.array(manifestConflictSchema.extend({ keep: conflictSideSchema })).default([]),
1326
+ });
1327
+
1328
+ /** What a preview answers: the changes, and the conflicts nothing resolved. */
1329
+ export const manifestPreviewSchema = manifestChangesSchema.extend({
1330
+ conflicts: z.array(manifestConflictSchema),
1331
+ });
1332
+
961
1333
  /**
962
1334
  * An apply's changes, and a fire secret, shown this once, for each trigger
963
1335
  * it left with none anyone holds: one it created, or one whose