@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.
- package/README.md +94 -26
- package/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/commands/apply.ts +171 -44
- package/src/commands/management.ts +41 -22
- package/src/commands/new.ts +48 -25
- package/src/commands/setup.ts +611 -0
- package/src/contracts/datasets/dataset-sql.ts +22 -5
- package/src/contracts/datasets/datasets.ts +139 -16
- package/src/contracts/egress-policy/scrub.ts +11 -8
- package/src/contracts/json/index.ts +21 -0
- package/src/contracts/manifest/index.ts +223 -55
- package/src/contracts/server/index.ts +109 -9
- package/src/contracts/server/reserved-names.fixtures.json +1 -3
- package/src/contracts/wire/index.ts +42 -3
- package/src/management/client.ts +14 -0
- package/src/management/profiles.ts +6 -4
- package/src/scaffold/files.ts +183 -144
- package/src/scaffold/ports.ts +5 -6
- package/src/scaffold/templates/cspell.json.txt +41 -0
- package/src/scaffold/templates/dockerignore.txt +3 -4
- package/src/scaffold/templates/gitignore.txt +1 -2
- package/src/scaffold/templates/setup.ts.txt +18 -26
- package/src/ui/prompts.ts +32 -10
|
@@ -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({
|
|
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
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
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.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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`
|
|
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;
|
|
456
|
-
*
|
|
457
|
-
*
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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`
|
|
490
|
-
* on a
|
|
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`
|
|
519
|
-
*
|
|
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; `
|
|
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
|
-
|
|
667
|
+
requiresApproval: z.boolean().default(false),
|
|
585
668
|
});
|
|
586
669
|
|
|
587
|
-
const manifestToolSchema = z
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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:
|
|
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
|
-
/**
|
|
943
|
+
/** Lets `searchRegistries` search the knowledge in the collections the agent sees. */
|
|
810
944
|
ragEnabled: z.boolean().default(false),
|
|
811
|
-
/**
|
|
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(
|
|
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 {
|
|
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
|
-
"
|
|
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(
|
|
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.
|
|
139
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
});
|