@m6d/cortex-cli 0.13.0 → 0.15.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.
@@ -7,6 +7,7 @@ import {
7
7
  IDENTIFIER_PATTERN,
8
8
  localeSchema,
9
9
  SERVER_EVENTS,
10
+ refuseBlockedApprovalTitle,
10
11
  serverToolSchema,
11
12
  } from "../server/index";
12
13
  import { MESSAGE_TRIGGER_NAME } from "../triggers/index";
@@ -45,8 +46,16 @@ export const TOOL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
45
46
  */
46
47
  export const RESPONSE_WRAPS = ["items", "value"] as const;
47
48
 
49
+ /** How OpenAPI serializes a query parameter: `form` (the default), `spaceDelimited`, `pipeDelimited` or `deepObject`. */
50
+ export const QUERY_STYLES = ["form", "spaceDelimited", "pipeDelimited", "deepObject"] as const;
51
+ /** How OpenAPI serializes a path parameter: `simple` (the default), `label` or `matrix`. */
52
+ export const PATH_STYLES = ["simple", "label", "matrix"] as const;
53
+
48
54
  export const SOAP_VERSIONS = ["1.1", "1.2"] as const;
49
55
 
56
+ /** An RFC 6901 pointer into a JSON document, naming a member: `/extra`, `/data/items`. */
57
+ const jsonPointerSchema = z.string().regex(/^\//).max(256);
58
+
50
59
  type Bounds = { min: number; max: number; default: number };
51
60
 
52
61
  /**
@@ -90,6 +99,25 @@ function setting({ min, max, default: fallback }: Bounds) {
90
99
  return z.number().int().min(min).max(max).default(fallback);
91
100
  }
92
101
 
102
+ /**
103
+ * The fields of an entry users may change in Cortex after an apply, each
104
+ * once, from the ones `fields` allows; none when left out. Every other field
105
+ * is the manifest's alone. An apply keeps a live change to a listed field
106
+ * unless the manifest changed that field too (a conflict, see
107
+ * `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.
110
+ */
111
+ function overridable<const TField extends string>(fields: readonly [TField, ...TField[]]) {
112
+ return z
113
+ .array(z.enum(fields))
114
+ .refine(
115
+ uniqueBy((field: TField) => field),
116
+ "a field repeats",
117
+ )
118
+ .default([]);
119
+ }
120
+
93
121
  /** Consumer-facing slugs: the manifest id, service keys, skill names and trigger names share it. */
94
122
  export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
95
123
  const slugSchema = z.string().regex(SLUG_PATTERN).max(64);
@@ -174,6 +202,16 @@ export const soapActionSchema = z.string().regex(/^[^"\\\u0000-\u001f\u007f]*$/)
174
202
  /** An envelope template: XML, so never only whitespace. */
175
203
  const envelopeTemplateSchema = z.string().refine((template) => template.trim().length > 0);
176
204
 
205
+ /**
206
+ * The media types a request body may be sent in besides JSON, the default.
207
+ * Each body field is written as its target says.
208
+ */
209
+ export const FORM_MEDIA_TYPES = [
210
+ "application/x-www-form-urlencoded",
211
+ "multipart/form-data",
212
+ ] as const;
213
+ export type FormMediaType = (typeof FORM_MEDIA_TYPES)[number];
214
+
177
215
  /**
178
216
  * The per-type payload a tool stores and publishes. Soap: the envelope
179
217
  * template is the full request XML with `{{field}}` placeholders bound to
@@ -181,7 +219,12 @@ const envelopeTemplateSchema = z.string().refine((template) => template.trim().l
181
219
  * acquires once per request.
182
220
  */
183
221
  export const toolConfigByType = {
184
- http: z.object({ responseWrap: z.enum(RESPONSE_WRAPS).optional() }),
222
+ http: z.object({
223
+ responseWrap: z.enum(RESPONSE_WRAPS).optional(),
224
+ requestMediaType: z.enum(FORM_MEDIA_TYPES).optional(),
225
+ /** Where a successful response keeps the result, for an API that wraps every answer (`{ success, extra }`); read before `responseWrap`. */
226
+ resultPath: jsonPointerSchema.optional(),
227
+ }),
185
228
  embedded: z.object({ embedOrigin: embedOriginSchema, surface: z.enum(TOOL_SURFACES) }),
186
229
  soap: z.object({
187
230
  soapVersion: z.enum(SOAP_VERSIONS),
@@ -342,17 +385,44 @@ export function sourceFits(
342
385
  }
343
386
 
344
387
  /**
345
- * Where a top-level scalar input field travels in the upstream request;
346
- * absent, path placeholders by name, then the query string on GET and the
347
- * body otherwise. `name` renames a query parameter, header or cookie on the
348
- * wire.
388
+ * Where a top-level input field travels in the upstream request; absent,
389
+ * path placeholders by name, then the query string on GET and the body
390
+ * otherwise. `name` renames a query parameter, header or cookie on the wire,
391
+ * and is the name `matrix` style writes into a path.
392
+ * `style` and `explode` write an array or object value as an OpenAPI
393
+ * parameter declares; left out, OpenAPI's defaults hold (`form` exploded for
394
+ * a query, `simple` unexploded for a path and a header). A cookie joins items
395
+ * with commas. In a form body (the tool's `requestMediaType`), a JSON
396
+ * `contentType` sends the value as JSON text, and `style` and `explode`
397
+ * write it as in a query; a multipart body repeats an array's items as
398
+ * parts, typing an object JSON unless `contentType` says text. A JSON body
399
+ * reads none of them.
349
400
  */
350
401
  export const fieldTargetSchema = z.discriminatedUnion("kind", [
351
- z.object({ kind: z.literal("body") }),
352
- z.object({ kind: z.literal("query"), name: z.string().trim().min(1).max(120).optional() }),
353
- z.object({ kind: z.literal("path") }),
354
- z.object({ kind: z.literal("header"), name: headerNameSchema.optional() }),
355
- z.object({ kind: z.literal("cookie"), name: headerNameSchema.optional() }),
402
+ z.strictObject({
403
+ kind: z.literal("body"),
404
+ contentType: z.enum(["application/json", "text/plain"]).optional(),
405
+ style: z.enum(QUERY_STYLES).optional(),
406
+ explode: z.boolean().optional(),
407
+ }),
408
+ z.strictObject({
409
+ kind: z.literal("query"),
410
+ name: z.string().trim().min(1).max(120).optional(),
411
+ style: z.enum(QUERY_STYLES).optional(),
412
+ explode: z.boolean().optional(),
413
+ }),
414
+ z.strictObject({
415
+ kind: z.literal("path"),
416
+ name: z.string().trim().min(1).max(120).optional(),
417
+ style: z.enum(PATH_STYLES).optional(),
418
+ explode: z.boolean().optional(),
419
+ }),
420
+ z.strictObject({
421
+ kind: z.literal("header"),
422
+ name: headerNameSchema.optional(),
423
+ explode: z.boolean().optional(),
424
+ }),
425
+ z.strictObject({ kind: z.literal("cookie"), name: headerNameSchema.optional() }),
356
426
  ]);
357
427
  export type FieldTarget = z.infer<typeof fieldTargetSchema>;
358
428
 
@@ -380,8 +450,8 @@ const itemsSchema = z.strictObject({
380
450
  * a type (with `"null"` beside it when the value may be null), an `enum` of
381
451
  * strings for a string, an object's `properties` and `required`, an
382
452
  * array's `items`, a `description`, and the extensions `x-sensitive`
383
- * (redacted from logs), `x-source` and `x-target` (top-level scalar fields
384
- * of a tool's input only).
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).
385
455
  */
386
456
  const schemaPropertySchema = z.strictObject({
387
457
  type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
@@ -452,9 +522,9 @@ function enumIssueOf(property: SchemaProperty, type: string | undefined) {
452
522
  /**
453
523
  * What one property breaks of itself: an `enum` off a string, with no value,
454
524
  * or listing `null` when the type does not allow it or the other way round;
455
- * `items` off an array or an array without them; and `x-source` or
456
- * `x-target` off a scalar of a `bindable` node, or a source that does not
457
- * fit the field.
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.
458
528
  */
459
529
  function propertyIssues(property: SchemaProperty, bindable: boolean) {
460
530
  const issues = new Array<RowIssue>();
@@ -468,13 +538,17 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
468
538
  });
469
539
  }
470
540
  const binds = bindable && SCALAR_TYPES.some((scalar) => scalar === type);
471
- for (const binding of ["x-source", "x-target"] as const) {
472
- if (property[binding] !== undefined && !binds) {
473
- issues.push({
474
- at: [binding],
475
- message: "only a top-level scalar field of a tool's input has it",
476
- });
477
- }
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) {
548
+ issues.push({
549
+ at: ["x-target"],
550
+ message: "only a top-level field of a tool's input has it",
551
+ });
478
552
  }
479
553
  const source = property["x-source"];
480
554
  if (binds && source && !sourceFits(source, { type, enum: property.enum })) {
@@ -486,8 +560,8 @@ function propertyIssues(property: SchemaProperty, bindable: boolean) {
486
560
  /**
487
561
  * Refuses, at every depth below `path`, what the console's field rows cannot
488
562
  * hold, since it stores a schema as rows and would drop or change it in
489
- * silence (`nodeIssues`, `propertyIssues`); `x-source` and `x-target` only
490
- * on a scalar directly under a node where `bindable`.
563
+ * silence (`nodeIssues`, `propertyIssues`); `x-source` only on a scalar,
564
+ * and `x-target` only on a field, directly under a node where `bindable`.
491
565
  */
492
566
  function heldByRows(
493
567
  node: {
@@ -515,8 +589,8 @@ function heldByRows(
515
589
 
516
590
  /**
517
591
  * An object's JSON Schema, the shape of a tool's or trigger's input and
518
- * output. `bindable` admits `x-source` and `x-target` on its top-level
519
- * scalar fields, as a tool's input has them.
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.
520
594
  */
521
595
  function objectSchemaOf(bindable: boolean) {
522
596
  return z
@@ -571,9 +645,18 @@ const toolBaseSchema = z.strictObject({
571
645
  pinned: z.boolean().default(false),
572
646
  tags: toolTagsSchema.default([]),
573
647
  audience: audienceSchema,
648
+ overridable: overridable([
649
+ "description",
650
+ "timeoutMs",
651
+ "maxResponseBytes",
652
+ "rateLimitRequests",
653
+ "rateLimitWindowSeconds",
654
+ "pinned",
655
+ "tags",
656
+ ]),
574
657
  });
575
658
 
576
- /** A tool that calls an endpoint; `approval` set, every call waits for the end user to approve it. */
659
+ /** A tool that calls an endpoint; with `requiresApproval`, every call waits for the end user to approve it. */
577
660
  const calledToolSchema = toolBaseSchema.extend({
578
661
  inputSchema: objectSchemaOf(true).default(EMPTY_OBJECT),
579
662
  url: upstreamUrlSchema.refine(
@@ -581,33 +664,68 @@ const calledToolSchema = toolBaseSchema.extend({
581
664
  "a {parameter} outside the path or query",
582
665
  ),
583
666
  credentials: credentialsSchema,
584
- approval: serverToolSchema.shape.approval,
667
+ requiresApproval: z.boolean().default(false),
585
668
  });
586
669
 
587
- const manifestToolSchema = z.discriminatedUnion("type", [
588
- calledToolSchema.extend({
589
- type: z.literal("http"),
590
- method: z.enum(TOOL_METHODS),
591
- readOnly: z.boolean(),
592
- ...toolConfigByType.http.shape,
593
- }),
594
- /** The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it. */
595
- calledToolSchema.extend({
596
- type: z.literal("embedded"),
597
- method: z.enum(TOOL_METHODS),
598
- ...toolConfigByType.embedded.shape,
599
- }),
600
- /** Always POSTed. */
601
- calledToolSchema.extend({
602
- type: z.literal("soap"),
603
- readOnly: z.boolean(),
604
- ...toolConfigByType.soap.shape,
605
- }),
606
- /** Answered by your app: the model calls it and the widget hands the call over. */
607
- toolBaseSchema.extend({ type: z.literal("client") }),
608
- ]);
670
+ const manifestToolSchema = z
671
+ .discriminatedUnion("type", [
672
+ calledToolSchema.extend({
673
+ type: z.literal("http"),
674
+ method: z.enum(TOOL_METHODS),
675
+ readOnly: z.boolean(),
676
+ ...toolConfigByType.http.shape,
677
+ }),
678
+ /** The endpoint is the initiate call that opens the page; never read-only, so a reload rejoins it. */
679
+ calledToolSchema.extend({
680
+ type: z.literal("embedded"),
681
+ method: z.enum(TOOL_METHODS),
682
+ ...toolConfigByType.embedded.shape,
683
+ }),
684
+ /** Always POSTed. */
685
+ calledToolSchema.extend({
686
+ type: z.literal("soap"),
687
+ readOnly: z.boolean(),
688
+ ...toolConfigByType.soap.shape,
689
+ }),
690
+ /** Answered by your app: the model calls it and the widget hands the call over. */
691
+ toolBaseSchema.extend({ type: z.literal("client") }),
692
+ ])
693
+ .superRefine((tool, context) => {
694
+ if (tool.type !== "client") refuseBlockedApprovalTitle(tool, context, []);
695
+ });
609
696
  export type ManifestTool = z.input<typeof manifestToolSchema>;
610
697
 
698
+ const toolNamesSchema = z.array(serverToolSchema.shape.name).max(500).default([]);
699
+
700
+ /**
701
+ * An OpenAPI document whose operations become HTTP tools this manifest owns.
702
+ * Each apply reads it, imports every operation Cortex supports, keeps those
703
+ * 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
705
+ * method waits for the end user's approval, unless `approval` names the
706
+ * tool the other way.
707
+ */
708
+ const manifestOpenApiSchema = z.strictObject({
709
+ /** Names the source in what an apply reports. */
710
+ name: nameSchema,
711
+ /** Where the document is read, with no credentials: OpenAPI 3 or Swagger 2, JSON or YAML. */
712
+ url: upstreamUrlSchema,
713
+ credentials: credentialsSchema,
714
+ /** Where every operation's successful response keeps its result, when the API wraps each answer. */
715
+ resultPath: jsonPointerSchema.optional(),
716
+ /** Tools listed in every turn of every agent they reach, whatever the question. */
717
+ pinned: toolNamesSchema,
718
+ approval: z
719
+ .strictObject({ never: toolNamesSchema, always: toolNamesSchema })
720
+ .prefault({})
721
+ .refine(
722
+ ({ never, always }) => !never.some((name) => always.includes(name)),
723
+ "a tool both never and always needs approval",
724
+ ),
725
+ audience: audienceSchema,
726
+ });
727
+ export type ManifestOpenApi = z.input<typeof manifestOpenApiSchema>;
728
+
611
729
  /** Identified by its `key`, which skills and collections scope themselves by; `name` is its label. */
612
730
  const manifestServiceSchema = z.strictObject({
613
731
  /** What consumers parameterize their tools with; fixed once published. */
@@ -616,6 +734,7 @@ const manifestServiceSchema = z.strictObject({
616
734
  /** Rich text: it may mention tools and skills. */
617
735
  description: localizedTextSchema,
618
736
  audience: audienceSchema,
737
+ overridable: overridable(["description"]),
619
738
  });
620
739
  export type ManifestService = z.input<typeof manifestServiceSchema>;
621
740
 
@@ -627,6 +746,7 @@ const manifestSkillSchema = z.strictObject({
627
746
  /** The key of the service the skill renders inside; fixed once created. */
628
747
  service: slugSchema.optional(),
629
748
  audience: audienceSchema,
749
+ overridable: overridable(["description", "body"]),
630
750
  });
631
751
  export type ManifestSkill = z.input<typeof manifestSkillSchema>;
632
752
 
@@ -643,7 +763,7 @@ export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
643
763
  z.object({ policy: z.literal("single") }),
644
764
  z.object({ policy: z.literal("per-firing") }),
645
765
  z.object({ policy: z.literal("ephemeral") }),
646
- z.object({ policy: z.literal("keyed"), key: z.string().regex(/^\//).max(256) }),
766
+ z.object({ policy: z.literal("keyed"), key: jsonPointerSchema }),
647
767
  ]);
648
768
  export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
649
769
 
@@ -674,6 +794,14 @@ const manifestTriggerSchema = z.strictObject({
674
794
  /** The model its runs talk to, over each agent's. */
675
795
  model: modelSchema.optional(),
676
796
  audience: audienceSchema,
797
+ overridable: overridable([
798
+ "description",
799
+ "instructions",
800
+ "threadPolicy",
801
+ "budgetPerDay",
802
+ "disableAfterFailures",
803
+ "model",
804
+ ]),
677
805
  });
678
806
  export type ManifestTrigger = z.input<typeof manifestTriggerSchema>;
679
807
 
@@ -697,6 +825,7 @@ const manifestDatasetHostSchema = z
697
825
  /** Among those, the ones listed in every turn, whatever the question. */
698
826
  pinned: z.array(datasetIdentifierSchema).max(50).default([]),
699
827
  audience: audienceSchema,
828
+ overridable: overridable(["timeoutMs"]),
700
829
  })
701
830
  .superRefine((host, context) => {
702
831
  const unexposed = unexposedPins(host);
@@ -737,6 +866,7 @@ const manifestMcpServerSchema = z.strictObject({
737
866
  include: mcpToolNamesSchema.default([]),
738
867
  exclude: mcpToolNamesSchema.default([]),
739
868
  audience: audienceSchema,
869
+ overridable: overridable(["include", "exclude"]),
740
870
  });
741
871
  export type ManifestMcpServer = z.input<typeof manifestMcpServerSchema>;
742
872
 
@@ -759,6 +889,7 @@ const manifestWebhookSchema = z.strictObject({
759
889
  ),
760
890
  timeoutMs: setting(SETTING_BOUNDS.webhook.timeoutMs),
761
891
  audience: audienceSchema,
892
+ overridable: overridable(["description", "timeoutMs"]),
762
893
  });
763
894
  export type ManifestWebhook = z.input<typeof manifestWebhookSchema>;
764
895
 
@@ -768,7 +899,10 @@ const manifestCollectionSchema = z.strictObject({
768
899
  description: z.string().trim().max(2_000).default(""),
769
900
  /** The key of the service every document in the collection belongs to. */
770
901
  service: slugSchema.optional(),
902
+ /** Whether answers may name its documents; false keeps their names, sections and pages from the model. */
903
+ citable: z.boolean().default(true),
771
904
  audience: audienceSchema,
905
+ overridable: overridable(["description"]),
772
906
  });
773
907
  export type ManifestCollection = z.input<typeof manifestCollectionSchema>;
774
908
 
@@ -806,10 +940,11 @@ const { agent: agentBounds, agentContext, agentLimits } = SETTING_BOUNDS;
806
940
 
807
941
  /** How the agent runs; every setting left out keeps its default. */
808
942
  const agentSettingsSchema = z.strictObject({
809
- /** Offers `searchKnowledge` over the collections the agent sees. */
943
+ /** Lets `searchRegistries` search the knowledge in the collections the agent sees. */
810
944
  ragEnabled: z.boolean().default(false),
811
- /** Offers `searchTools` over the tools the agent sees, beyond the pinned and mentioned ones. */
945
+ /** Lets `searchRegistries` search the tools the agent sees, beyond the pinned and mentioned ones. */
812
946
  toolRegistryEnabled: z.boolean().default(false),
947
+ /** Lets `searchRegistries` search the services the agent sees. */
813
948
  serviceRegistryEnabled: z.boolean().default(false),
814
949
  attachmentsEnabled: z.boolean().default(true),
815
950
  /** Hosts the built-in `fetchImage` tool may pull images from; none, no such tool. */
@@ -869,6 +1004,7 @@ const manifestAgentSchema = z.strictObject({
869
1004
  settings: agentSettingsSchema.prefault({}),
870
1005
  suggestions: suggestionsSchema.default({ en: [], ar: [] }),
871
1006
  greeting: greetingSchema.default({ en: "", ar: "" }),
1007
+ overridable: overridable(["name", "prompt", "models", "settings", "suggestions", "greeting"]),
872
1008
  });
873
1009
  export type ManifestAgent = z.input<typeof manifestAgentSchema>;
874
1010
 
@@ -889,13 +1025,16 @@ export const CORTEX_MANIFEST_ID = "cortex";
889
1025
  /**
890
1026
  * A deployment's agents and components, owned by the manifest `id`: an apply
891
1027
  * of it creates what is new, updates what changed, and deletes what it no
892
- * longer lists, touching nothing it did not create.
1028
+ * longer lists, touching nothing it did not create. What an entry lists as
1029
+ * `overridable` may then change in Cortex too, and an apply keeps that
1030
+ * change unless the manifest changed the same field.
893
1031
  */
894
1032
  export const manifestSchema = z.strictObject({
895
1033
  /** Fixed for the deployment it describes: the rows an apply owns carry it. */
896
1034
  id: slugSchema,
897
1035
  agents: listOf(manifestAgentSchema, (agent) => agent.slug),
898
1036
  tools: listOf(manifestToolSchema, byName),
1037
+ openapi: listOf(manifestOpenApiSchema, byName),
899
1038
  services: listOf(manifestServiceSchema, (service) => service.key),
900
1039
  skills: listOf(manifestSkillSchema, byName),
901
1040
  triggers: listOf(manifestTriggerSchema, byName),
@@ -907,8 +1046,11 @@ export const manifestSchema = z.strictObject({
907
1046
  /** A manifest as written: every field with a default may be left out. */
908
1047
  export type Manifest = z.input<typeof manifestSchema>;
909
1048
 
1049
+ /** The kinds of row a manifest owns; an OpenAPI source's operations are `tools`. */
1050
+ const manifestKindSchema = manifestSchema.keyof().exclude(["id", "openapi"]);
1051
+
910
1052
  /** Entry names per kind: agents by slug, services by key, everything else by name. */
911
- const namesByKindSchema = z.record(manifestSchema.keyof().exclude(["id"]), z.array(z.string()));
1053
+ const namesByKindSchema = z.record(manifestKindSchema, z.array(z.string()));
912
1054
 
913
1055
  /** What an apply creates, updates and deletes, kind by kind; a preview answers the same without doing it. */
914
1056
  export const manifestChangesSchema = z.object({
@@ -917,6 +1059,32 @@ export const manifestChangesSchema = z.object({
917
1059
  delete: namesByKindSchema,
918
1060
  });
919
1061
 
1062
+ /** A field of one entry that both the manifest and a live write changed since the last apply. */
1063
+ const manifestConflictSchema = z.strictObject({
1064
+ kind: manifestKindSchema,
1065
+ name: z.string(),
1066
+ field: z.string(),
1067
+ });
1068
+
1069
+ /** Which value a conflict keeps: the one changed live in Cortex, or the manifest's. */
1070
+ const conflictSideSchema = z.enum(["live", "manifest"]);
1071
+
1072
+ /**
1073
+ * What preview and sync take: the manifest, and how its conflicts resolve.
1074
+ * A conflict takes its own entry in `resolutions`, else `prefer`; a sync
1075
+ * left with one neither answers is refused as `unresolved_conflicts`,
1076
+ * naming them. A resolution for a field that no longer conflicts is ignored.
1077
+ */
1078
+ export const manifestApplyInputSchema = manifestSchema.extend({
1079
+ prefer: conflictSideSchema.optional(),
1080
+ resolutions: z.array(manifestConflictSchema.extend({ keep: conflictSideSchema })).default([]),
1081
+ });
1082
+
1083
+ /** What a preview answers: the changes, and the conflicts nothing resolved. */
1084
+ export const manifestPreviewSchema = manifestChangesSchema.extend({
1085
+ conflicts: z.array(manifestConflictSchema),
1086
+ });
1087
+
920
1088
  /**
921
1089
  * An apply's changes, and a fire secret, shown this once, for each trigger
922
1090
  * it left with none anyone holds: one it created, or one whose
@@ -2,7 +2,8 @@ import { z } from "zod";
2
2
  import { REQUEST_LOCATION_TOOL } from "../client-tools/index";
3
3
  import { backendErrorSchema } from "../errors/index";
4
4
  import { pendingApprovalSchema, TRIGGER_ANSWER_TOOL_NAME } from "../triggers/index";
5
- import { LOCALES } from "../wire/index";
5
+ import { isRecord, unescapePointerToken } from "../json/index";
6
+ import { APPROVAL_TITLE_FIELD, LOCALES } from "../wire/index";
6
7
 
7
8
  /*
8
9
  * The server protocol: what Cortex calls on a customer's backend. `GET /`
@@ -96,10 +97,8 @@ export const BUILT_IN_TOOL_NAMES = [
96
97
  "queryDataset",
97
98
  "readAttachment",
98
99
  "readReport",
99
- "searchKnowledge",
100
- "searchServices",
100
+ "searchRegistries",
101
101
  "searchThreadHistory",
102
- "searchTools",
103
102
  REQUEST_LOCATION_TOOL,
104
103
  TRIGGER_ANSWER_TOOL_NAME,
105
104
  ] as const;
@@ -108,11 +107,14 @@ export function isBuiltInToolName(name: string) {
108
107
  return BUILT_IN_TOOL_NAMES.some((builtIn) => builtIn === name);
109
108
  }
110
109
 
110
+ /** The longest name a tool takes, a server's or one defined in Cortex. */
111
+ export const TOOL_NAME_MAX_LENGTH = 64;
112
+
111
113
  /** A tool name, a server's or one defined in Cortex: an identifier no built-in tool holds. */
112
114
  const declaredToolNameSchema = z
113
115
  .string()
114
116
  .regex(IDENTIFIER_PATTERN)
115
- .max(64)
117
+ .max(TOOL_NAME_MAX_LENGTH)
116
118
  .refine((name) => !isBuiltInToolName(name), "the name of a built-in tool");
117
119
 
118
120
  const jsonSchemaSchema = z.record(z.string(), z.unknown());
@@ -135,8 +137,9 @@ export type ServerVariable = z.infer<typeof serverVariableSchema>;
135
137
 
136
138
  /**
137
139
  * A tool the server runs. Its input is an object, the `input` its calls
138
- * carry. An `approval` sentence per locale parks every call for the end
139
- * user's approval.
140
+ * carry. `requiresApproval` parks every call for the end user's approval;
141
+ * such a tool's input cannot declare `APPROVAL_TITLE_FIELD`, which the
142
+ * model fills for the approval card.
140
143
  */
141
144
  export const serverToolSchema = z.object({
142
145
  name: declaredToolNameSchema,
@@ -147,7 +150,7 @@ export const serverToolSchema = z.object({
147
150
  ),
148
151
  outputSchema: jsonSchemaSchema.optional(),
149
152
  readOnly: z.boolean(),
150
- approval: z.record(localeSchema, z.string().trim().min(1).max(500)).optional(),
153
+ requiresApproval: z.boolean().optional(),
151
154
  });
152
155
  export type ServerTool = z.infer<typeof serverToolSchema>;
153
156
 
@@ -179,6 +182,97 @@ export const approvalRequestedEventSchema = z.object({
179
182
  });
180
183
  export type ApprovalRequestedEvent = z.infer<typeof approvalRequestedEventSchema>;
181
184
 
185
+ /** A JSON Pointer token as a key: percent-decoded, then unescaped; undefined when its encoding is broken. */
186
+ function pointerKey(token: string) {
187
+ try {
188
+ return unescapePointerToken(decodeURIComponent(token));
189
+ } catch {
190
+ return undefined;
191
+ }
192
+ }
193
+
194
+ /** What a local `$ref` (`#/$defs/order`) points at in `root`, read as the input compiler reads it; undefined for any other. */
195
+ function pointedAt(root: unknown, ref: string) {
196
+ if (!ref.startsWith("#")) return undefined;
197
+ let node = root;
198
+ for (const token of ref.slice(1).split("/").slice(1)) {
199
+ const key = pointerKey(token);
200
+ node =
201
+ key !== undefined && (isRecord(node) || Array.isArray(node))
202
+ ? new Map<string, unknown>(Object.entries(node)).get(key)
203
+ : undefined;
204
+ }
205
+ return node;
206
+ }
207
+
208
+ /** Whether one schema node names `APPROVAL_TITLE_FIELD` as a property or a required field. */
209
+ function namesApprovalTitle(node: Record<string, unknown>) {
210
+ const required: unknown = node.required;
211
+ return (
212
+ (isRecord(node.properties) && Object.hasOwn(node.properties, APPROVAL_TITLE_FIELD)) ||
213
+ (Array.isArray(required) && required.includes(APPROVAL_TITLE_FIELD))
214
+ );
215
+ }
216
+
217
+ /** What describes the same object as `node`: its `allOf`, `anyOf` and `oneOf` members, and what its local `$ref` points at in `root`. */
218
+ function sameObjectSchemas(node: Record<string, unknown>, root: unknown) {
219
+ const schemas: unknown[] = typeof node.$ref === "string" ? [pointedAt(root, node.$ref)] : [];
220
+ for (const keyword of ["allOf", "anyOf", "oneOf"]) {
221
+ const members: unknown = node[keyword];
222
+ if (!Array.isArray(members)) continue;
223
+ const listed: unknown[] = members;
224
+ schemas.push(...listed);
225
+ }
226
+ return schemas;
227
+ }
228
+
229
+ /**
230
+ * Every node that describes the same object as an input schema's root: the
231
+ * root, and what `sameObjectSchemas` reaches from it at any depth, each once.
232
+ */
233
+ function sameObjectNodes(inputSchema: unknown) {
234
+ const nodes = new Array<Record<string, unknown>>();
235
+ const pending = [inputSchema];
236
+ while (pending.length > 0) {
237
+ const node = pending.pop();
238
+ if (!isRecord(node) || nodes.includes(node)) continue;
239
+ nodes.push(node);
240
+ pending.push(...sameObjectSchemas(node, inputSchema));
241
+ }
242
+ return nodes;
243
+ }
244
+
245
+ /** Whether one schema node is closed to properties it does not declare. */
246
+ function closed(node: Record<string, unknown>) {
247
+ return node.additionalProperties === false || node.unevaluatedProperties === false;
248
+ }
249
+
250
+ /**
251
+ * Whether an input schema leaves no room for `APPROVAL_TITLE_FIELD`, which
252
+ * Cortex adds to the root of a tool that needs approval: a node describing
253
+ * the input names it, or one besides the root is closed to it, so a strict
254
+ * reading of the schema could not take the title.
255
+ */
256
+ export function blocksApprovalTitle(inputSchema: unknown) {
257
+ return sameObjectNodes(inputSchema).some(
258
+ (node) => namesApprovalTitle(node) || (node !== inputSchema && closed(node)),
259
+ );
260
+ }
261
+
262
+ /** Refuses, under `path`, a tool that needs approval whose input `blocksApprovalTitle`. */
263
+ export function refuseBlockedApprovalTitle(
264
+ tool: { requiresApproval?: boolean; inputSchema: unknown },
265
+ context: z.RefinementCtx,
266
+ path: Array<string | number>,
267
+ ) {
268
+ if (!tool.requiresApproval || !blocksApprovalTitle(tool.inputSchema)) return;
269
+ context.addIssue({
270
+ code: "custom",
271
+ path: [...path, "inputSchema"],
272
+ message: `a tool that needs approval cannot take an input named ${APPROVAL_TITLE_FIELD}, nor one a closed sub-schema describes: Cortex adds the approval card's title to it`,
273
+ });
274
+ }
275
+
182
276
  function uniqueNames(entries: ReadonlyArray<{ name: string }>) {
183
277
  return new Set(entries.map((entry) => entry.name)).size === entries.length;
184
278
  }
@@ -187,7 +281,13 @@ function uniqueNames(entries: ReadonlyArray<{ name: string }>) {
187
281
  export const serverManifestSchema = z.object({
188
282
  version: z.string().min(1).max(128),
189
283
  variables: z.array(serverVariableSchema).max(100).refine(uniqueNames, "variable names repeat"),
190
- tools: z.array(serverToolSchema).max(500).refine(uniqueNames, "tool names repeat"),
284
+ tools: z
285
+ .array(serverToolSchema)
286
+ .max(500)
287
+ .refine(uniqueNames, "tool names repeat")
288
+ .superRefine((tools, context) => {
289
+ tools.forEach((tool, index) => refuseBlockedApprovalTitle(tool, context, [index]));
290
+ }),
191
291
  verify: z.boolean(),
192
292
  events: z.array(z.enum(SERVER_EVENTS)),
193
293
  });
@@ -7,10 +7,8 @@
7
7
  "queryDataset",
8
8
  "readAttachment",
9
9
  "readReport",
10
- "searchKnowledge",
11
- "searchServices",
10
+ "searchRegistries",
12
11
  "searchThreadHistory",
13
- "searchTools",
14
12
  "requestLocation",
15
13
  "submitAnswer"
16
14
  ],